use std::collections::{HashMap, HashSet};
use std::fs;
use std::io::{self, BufRead, BufReader, Cursor, Read, Write};
use std::path::{Path, PathBuf};
use std::time::SystemTime;

use anyhow::{anyhow, bail, Context, Result};
use filetime::FileTime;
use chrono::NaiveDateTime;

/// 一条 diff hunk 中的行类型。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum HunkLineKind {
    Context,
    Add,
    Remove,
}

#[derive(Debug, Clone)]
struct HunkLine {
    kind: HunkLineKind,
    text: String,
}

#[derive(Debug, Clone)]
struct Hunk {
    old_start: usize,
    old_len: usize,
    new_start: usize,
    new_len: usize,
    header_suffix: Option<String>,
    lines: Vec<HunkLine>,
}

#[derive(Debug, Clone)]
struct HunkApplyReport {
    index: usize,
    hunk: Hunk,
    success: bool,
    at_line: usize,
    offset: isize,
    fuzz_used: i32,
}

#[derive(Debug, Default, Clone)]
struct FilePatch {
    old_path: Option<String>,
    new_path: Option<String>,
    old_label: Option<String>,
    new_label: Option<String>,
    index_line: Option<String>,
    source_format: Option<RejectFormat>,
    rename_from: Option<String>,
    rename_to: Option<String>,
    copy_from: Option<String>,
    copy_to: Option<String>,
    old_mode: Option<String>,
    new_mode: Option<String>,
    hunks: Vec<Hunk>,
    /// 从 +++（或 ---）行 \t 后的时间戳字符串，用于 -T/-Z 写回 mtime。
    timestamp_header: Option<String>,
    malformed_line: Option<usize>,
    malformed_text: Option<String>,
    git_binary_patch: bool,
}

#[derive(Debug, Clone)]
pub struct PatchConfig {
    pub input_patch: Option<PathBuf>,
    pub origfile: Option<PathBuf>,
    pub output: Option<PathBuf>,
    pub dry_run: bool,
    pub strip: Option<i32>,
    pub reverse: bool,
    pub reverse_specified: bool,
    pub fuzz: Option<i32>,
    pub backup: bool,
    pub backup_suffix: Option<String>,
    pub backup_prefix: Option<String>,
    /// -Y/--basename-prefix：备份文件 basename 前的前缀。
    pub basename_prefix: Option<String>,
    /// -V 或环境变量：simple | numbered | existing。
    pub version_control: Option<BackupType>,
    pub backup_if_mismatch: bool,
    pub no_backup_if_mismatch: bool,
    /// --merge：冲突时插入冲突标记而非拒绝。
    pub merge: bool,
    pub force: bool,
    pub batch: bool,
    /// -N/--forward：不自动尝试反向。
    pub noreverse: bool,
    pub reject_file: Option<PathBuf>,
    /// -d/--directory：先 chdir。
    pub directory: Option<PathBuf>,
    /// -g/--get：从 RCS 等取文件（正数则自动取）。
    pub patch_get: Option<i32>,
    /// -T/--set-time：写后设置修改时间为当前（本地）。
    pub set_time: bool,
    /// -Z/--set-utc：写后设置修改时间为当前 UTC。
    pub set_utc: bool,
    pub verbose: bool,
    pub quiet: bool,
    /// 强制解析格式（对应 -u/-c/-n/-e），为 None 时自动探测。
    pub format_hint: Option<DiffFormatHint>,
    /// 只读文件处理策略。
    pub read_only: Option<ReadOnlyMode>,
    /// reject 输出格式。
    pub reject_format: Option<RejectFormat>,
    /// -D/--ifdef NAME：按 hunk 输出 #ifndef NAME / #else / #endif 块。
    pub ifdef_name: Option<String>,
    /// -E/--remove-empty-files：打补丁后若文件为空则删除。
    pub remove_empty_files: bool,
    /// -l/--ignore-whitespace：将补丁与文件中的任意空白序列视为等价。
    pub ignore_whitespace: bool,
    /// --quoting-style：诊断中文件名的引号风格（literal/shell/shell-always/c/escape）。
    pub quoting_style: Option<String>,
    /// --follow-symlinks：打开文件时是否跟随符号链接（false 时用 O_NOFOLLOW）。
    pub follow_symlinks: bool,
    /// --binary：以二进制方式读写，不进行换行转换。
    pub binary: bool,
    /// --posix：POSIX 兼容模式。
    pub posix: bool,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BackupType {
    Simple,
    Numbered,
    Existing,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiffFormatHint {
    Unified,
    Context,
    Normal,
    Ed,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReadOnlyMode {
    Ignore,
    Warn,
    Fail,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RejectFormat {
    Context,
    Unified,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PatchExit {
    Ok = 0,
    SomeFailed = 1,
    Trouble = 2,
}

pub fn apply_patches(config: &PatchConfig) -> PatchExit {
    match apply_patches_inner(config) {
        Ok(SomeFailedFlag { some_failed }) => {
            if some_failed {
                PatchExit::SomeFailed
            } else {
                PatchExit::Ok
            }
        }
        Err(e) => {
            if e.to_string() == "__malformed_reported__" {
                return PatchExit::Trouble;
            }
            // 对齐 GNU patch：对“无法识别为有效补丁”的输入输出统一垃圾输入错误。
            let msg = e.to_string();
            if msg.starts_with("replacement text or line numbers mangled in hunk at line ") {
                let prog = std::env::args().next().unwrap_or_else(|| "patch".to_string());
                let _ = writeln!(io::stderr(), "{}: **** {}", prog, msg);
                return PatchExit::Trouble;
            }
            if msg.contains("补丁块缺少文件名头部")
                || msg.contains("补丁文件中未解析到任何文件块")
                || msg.contains("在缺少文件头（---/+++）的情况下遇到 hunk 头")
            {
                let prog = std::env::args().next().unwrap_or_else(|| "patch".to_string());
                let _ = writeln!(io::stderr(), "{}: **** Only garbage was found in the patch input.", prog);
            } else {
                let _ = writeln!(io::stderr(), "patch: {}", e);
            }
            PatchExit::Trouble
        }
    }
}

struct SomeFailedFlag {
    some_failed: bool,
}

fn apply_patches_inner(config: &PatchConfig) -> Result<SomeFailedFlag> {
    if let Some(ref d) = config.directory {
        std::env::set_current_dir(d).with_context(|| format!("无法切换目录 {}", d.display()))?;
    }

    let mut raw = String::new();
    match &config.input_patch {
        Some(path) => {
            let mut f = fs::File::open(path)
                .with_context(|| format!("无法打开补丁文件 {}", path.display()))?;
            f.read_to_string(&mut raw)
                .with_context(|| format!("无法读取补丁文件 {}", path.display()))?;
        }
        None => {
            io::stdin().read_to_string(&mut raw)?;
        }
    }
    if raw.is_empty() {
        // 兼容 GNU：空补丁配合 ORIGFILE 与 -o 时，复制输入文件到输出。
        if let (Some(orig), Some(out)) = (config.origfile.as_ref(), config.output.as_ref()) {
            if !config.quiet {
                let _ = writeln!(
                    io::stderr(),
                    "patching file {} (read from {})",
                    out.display(),
                    orig.display()
                );
            }
            let lines = read_lines(orig, config.follow_symlinks, config.binary)
                .unwrap_or_else(|_| Vec::new());
            if !config.dry_run {
                if let Some(parent) = out.parent() {
                    let _ = fs::create_dir_all(parent);
                }
                write_lines(out, &lines, config.binary)?;
            }
            return Ok(SomeFailedFlag { some_failed: false });
        }
        bail!("补丁输入为空");
    }
    if !config.binary && should_strip_patch_crlf(&raw) {
        let _ = writeln!(
            io::stderr(),
            "(Stripping trailing CRs from patch; use --binary to disable.)"
        );
        raw = raw.replace("\r\n", "\n");
    }

    // 兼容 GNU 行为：当用户显式给出 ORIGFILE 且补丁缺少 ---/+++ 头时，
    // 对于 unified/context hunk 自动补入同名头，允许应用到 ORIGFILE。
    if let Some(orig) = config.origfile.as_ref() {
        raw = synthesize_missing_headers_for_origfile(&raw, orig);
    }

    let mut reader: Box<dyn BufRead> = Box::new(BufReader::new(Cursor::new(raw.into_bytes())));

    let mut peek = String::new();
    if reader.read_line(&mut peek)? == 0 {
        bail!("补丁输入为空");
    }

    // 若用户使用 -e/-n/-c/-u 显式指定格式，则优先按指定格式解析。
    if let Some(fmt) = config.format_hint {
        let mut combined = io::Read::chain(Cursor::new(peek), reader);
        return match fmt {
            DiffFormatHint::Ed => {
                let target = config
                    .origfile
                    .as_ref()
                    .ok_or_else(|| anyhow!("ed 脚本需要指定 ORIGFILE"))?;
                apply_ed_script(&mut combined, target, config)
                    .map(|()| SomeFailedFlag { some_failed: false })
            }
            DiffFormatHint::Normal => {
                let file_patches = parse_normal_patches(&mut combined)?;
                apply_file_patches(config, file_patches, None)
            }
            DiffFormatHint::Context => {
                let (file_patches, prereq) = parse_context_patches(&mut combined)?;
                apply_file_patches(config, file_patches, prereq)
            }
            DiffFormatHint::Unified => {
                let (file_patches, prereq) = parse_unified_patches(&mut combined)?;
                apply_file_patches(config, file_patches, prereq)
            }
        };
    }

    // 未强制指定格式时，沿用启发式检测。
    if looks_like_ed_or_normal(&peek) {
        let second = {
            let mut s = String::new();
            reader.read_line(&mut s)?;
            s
        };
        if second.starts_with("< ") || second.starts_with("> ") {
            let mut combined =
                io::Read::chain(Cursor::new(peek), io::Read::chain(Cursor::new(second), reader));
            let file_patches = parse_normal_patches(&mut combined)?;
            return apply_file_patches(config, file_patches, None);
        }
        let mut combined =
            io::Read::chain(Cursor::new(peek), io::Read::chain(Cursor::new(second), reader));
        let target = config
            .origfile
            .as_ref()
            .ok_or_else(|| anyhow!("ed 脚本需要指定 ORIGFILE"))?;
        return apply_ed_script(&mut combined, target, config)
            .map(|()| SomeFailedFlag { some_failed: false });
    }

    let mut combined = io::Read::chain(Cursor::new(peek), reader);
    let (file_patches, prereq) = parse_patches(&mut combined)?;
    apply_file_patches(config, file_patches, prereq)
}

/// 按 --quoting-style 格式化路径，用于 stderr 诊断。
fn quote_path(path: &Path, style: Option<&str>) -> String {
    let s = path.display().to_string();
    match style {
        Some("literal") | None => s,
        Some("shell") => {
            if s.is_empty() || s.contains(' ') || s.contains('\'') || s.contains('"') || s.contains('\\') || s.contains('*') || s.contains('?') || s.contains('[') || s.contains(']') || s.contains('$') || s.contains('`') || s.contains('!') || s.contains('#') {
                shell_quote(&s)
            } else {
                s
            }
        }
        Some("shell-always") => shell_quote(&s),
        Some("c") => c_quote(&s),
        Some("escape") => escape_backslash(&s),
        _ => s,
    }
}

fn shell_quote(s: &str) -> String {
    let mut o = String::from("'");
    for c in s.chars() {
        if c == '\'' {
            o.push_str("'\\''");
        } else {
            o.push(c);
        }
    }
    o.push('\'');
    o
}

fn c_quote(s: &str) -> String {
    let mut o = String::from("\"");
    for c in s.chars() {
        match c {
            '\\' => o.push_str("\\\\"),
            '"' => o.push_str("\\\""),
            '\n' => o.push_str("\\n"),
            '\t' => o.push_str("\\t"),
            '\r' => o.push_str("\\r"),
            _ if c.is_ascii() && (c as u32) >= 32 && (c as u32) < 127 => o.push(c),
            _ => o.push_str(&format!("\\{:03o}", c as u32)),
        }
    }
    o.push('"');
    o
}

fn escape_backslash(s: &str) -> String {
    let mut o = String::new();
    for c in s.chars() {
        if c == '\\' {
            o.push_str("\\\\");
        } else {
            o.push(c);
        }
    }
    o
}

fn looks_like_ed_or_normal(line: &str) -> bool {
    let t = line.trim();
    if t.is_empty() {
        return false;
    }
    let mut i = 0;
    let bytes = t.as_bytes();
    while i < bytes.len() && bytes[i].is_ascii_digit() {
        i += 1;
    }
    if i == 0 {
        return false;
    }
    if i < bytes.len() && bytes[i] == b',' {
        i += 1;
        while i < bytes.len() && bytes[i].is_ascii_digit() {
            i += 1;
        }
    }
    if i >= bytes.len() {
        return false;
    }
    matches!(bytes[i], b'a' | b'd' | b'c')
}

fn apply_file_patches(
    config: &PatchConfig,
    file_patches: Vec<FilePatch>,
    prereq: Option<String>,
) -> Result<SomeFailedFlag> {
    if file_patches.is_empty() {
        bail!("补丁文件中未解析到任何文件块");
    }
    let mut some_failed = false;
    let mut file_cache: HashMap<PathBuf, Vec<String>> = HashMap::new();
    let mut source_snapshot: HashMap<PathBuf, Vec<String>> = HashMap::new();
    let mut written_rejects: HashSet<PathBuf> = HashSet::new();
    let mut prereq_checked = false;

    fn hunk_word(n: usize) -> &'static str {
        if n == 1 { "hunk" } else { "hunks" }
    }

    for fp in file_patches {
        let rename_from = fp
            .rename_from
            .as_deref()
            .map(|p| map_patch_path_to_fs(p, config.strip))
            .transpose()?;
        let rename_to = fp
            .rename_to
            .as_deref()
            .map(|p| map_patch_path_to_fs(p, config.strip))
            .transpose()?;
        let copy_from = fp
            .copy_from
            .as_deref()
            .map(|p| map_patch_path_to_fs(p, config.strip))
            .transpose()?;
        let copy_to = fp
            .copy_to
            .as_deref()
            .map(|p| map_patch_path_to_fs(p, config.strip))
            .transpose()?;

        let mut remembered_backup_path: Option<PathBuf> = None;
        let (target_path, source_path, op_desc) = if let Some(orig) = &config.origfile {
            (orig.clone(), None, None)
        } else if !config.reverse {
            if let (Some(from), Some(to)) = (rename_from.clone(), rename_to.clone()) {
                (to, Some(from), Some("rename"))
            } else if let (Some(from), Some(to)) = (copy_from.clone(), copy_to.clone()) {
                (to, Some(from), Some("copy"))
            } else {
                // GNU patch 兼容：在 old/new 都可用时，按存在性与 .orig 场景选择更合理目标。
                let old_mapped = fp
                    .old_path
                    .as_deref()
                    .filter(|p| *p != "/dev/null")
                    .map(|p| map_patch_path_to_fs(p, config.strip))
                    .transpose()?;
                let new_mapped = fp
                    .new_path
                    .as_deref()
                    .filter(|p| *p != "/dev/null")
                    .map(|p| map_patch_path_to_fs(p, config.strip))
                    .transpose()?;

                let target = match (old_mapped, new_mapped) {
                    (Some(o), Some(n)) => {
                        let old_overstripped = fp
                            .old_path
                            .as_deref()
                            .map(|p| is_overstripped_patch_path(p, config.strip))
                            .unwrap_or(false);
                        let new_overstripped = fp
                            .new_path
                            .as_deref()
                            .map(|p| is_overstripped_patch_path(p, config.strip))
                            .unwrap_or(false);
                        if old_overstripped && new_overstripped {
                            PathBuf::from(".patch-nofile-sentinel")
                        } else if o
                            .file_name()
                            .and_then(|s| s.to_str())
                            .map(|s| is_remembered_backup_name(s, config))
                            .unwrap_or(false)
                        {
                            remembered_backup_path = Some(o.clone());
                            n
                        } else if old_overstripped && !new_overstripped {
                            n
                        } else if o.exists() && !n.exists() {
                            o
                        } else if n.exists() && !o.exists() {
                            n
                        } else {
                            o
                        }
                    }
                    (Some(o), None) => o,
                    (None, Some(n)) => n,
                    (None, None) => {
                        if config.format_hint == Some(DiffFormatHint::Normal) {
                            // 显式 -n 且未给 ORIGFILE：按 GNU no-file 语义返回状态 1。
                            some_failed = true;
                            let _ = writeln!(io::stderr(), "can't find file to patch at input line 1");
                            let _ = writeln!(io::stderr(), "No file to patch.  Skipping patch.");
                            let _ = writeln!(io::stderr(), "1 out of 1 hunk ignored");
                            continue;
                        }
                        bail!("补丁块缺少文件名头部（---/+++）");
                    }
                };
                (target, None, None)
            }
        } else if let (Some(from), Some(to)) = (rename_from.clone(), rename_to.clone()) {
            // -R 时反向处理 rename。
            (from, Some(to), Some("rename"))
        } else {
            // 路径选择逻辑与正向相同，-R 只影响 hunk 内容（由 swap_hunk_for_reverse 处理）
            let old_mapped = fp
                .old_path
                .as_deref()
                .filter(|p| *p != "/dev/null")
                .map(|p| map_patch_path_to_fs(p, config.strip))
                .transpose()?;
            let new_mapped = fp
                .new_path
                .as_deref()
                .filter(|p| *p != "/dev/null")
                .map(|p| map_patch_path_to_fs(p, config.strip))
                .transpose()?;

            let target = match (old_mapped, new_mapped) {
                (Some(o), Some(n)) => {
                    let old_overstripped = fp
                        .old_path
                        .as_deref()
                        .map(|p| is_overstripped_patch_path(p, config.strip))
                        .unwrap_or(false);
                    let new_overstripped = fp
                        .new_path
                        .as_deref()
                        .map(|p| is_overstripped_patch_path(p, config.strip))
                        .unwrap_or(false);
                    if old_overstripped && new_overstripped {
                        PathBuf::from(".patch-nofile-sentinel")
                    } else if o
                        .file_name()
                        .and_then(|s| s.to_str())
                        .map(|s| is_remembered_backup_name(s, config))
                        .unwrap_or(false)
                    {
                        remembered_backup_path = Some(o.clone());
                        n
                    } else if old_overstripped && !new_overstripped {
                        n
                    } else if o.exists() && !n.exists() {
                        o
                    } else if n.exists() && !o.exists() {
                        n
                    } else {
                        o
                    }
                }
                (Some(o), None) => o,
                (None, Some(n)) => n,
                (None, None) => {
                    if config.format_hint == Some(DiffFormatHint::Normal) {
                        some_failed = true;
                        let _ = writeln!(io::stderr(), "can't find file to patch at input line 1");
                        let _ = writeln!(io::stderr(), "No file to patch.  Skipping patch.");
                        let _ = writeln!(io::stderr(), "1 out of 1 hunk ignored");
                        continue;
                    }
                    bail!("补丁块缺少文件名头部（---/+++）");
                }
            };
            (target, None, None)
        };

        let source_for_read = if let Some(src) = source_path.as_ref() {
            if !target_path.exists() && src.exists() {
                src.clone()
            } else {
                target_path.clone()
            }
        } else {
            target_path.clone()
        };
        let effective_old_is_devnull = if config.reverse {
            fp.new_path.as_deref() == Some("/dev/null")
        } else {
            fp.old_path.as_deref() == Some("/dev/null")
        };
        let effective_new_is_devnull = if config.reverse {
            fp.old_path.as_deref() == Some("/dev/null")
        } else {
            fp.new_path.as_deref() == Some("/dev/null")
        };
        let effective_old_mode = if config.reverse {
            fp.new_mode.as_deref()
        } else {
            fp.old_mode.as_deref()
        };
        let effective_new_mode = if config.reverse {
            fp.old_mode.as_deref()
        } else {
            fp.new_mode.as_deref()
        };
        if fp.git_binary_patch {
            let shown = fp
                .new_path
                .as_deref()
                .or(fp.old_path.as_deref())
                .and_then(|p| map_patch_path_to_fs(p, config.strip).ok())
                .and_then(|p| p.file_name().map(|s| s.to_string_lossy().to_string()))
                .unwrap_or_else(|| target_path.display().to_string());
            some_failed = true;
            let _ = writeln!(io::stderr(), "File {}: git binary diffs are not supported.", shown);
            continue;
        }
        let fail_hunks_for_readonly = if config.reverse {
            fp.hunks.iter().map(swap_hunk_for_reverse).collect::<Vec<_>>()
        } else {
            fp.hunks.iter().cloned().collect::<Vec<_>>()
        };
        let mut readonly_forced_write = false;
        if let Some(mline) = fp.malformed_line {
            if !config.quiet && !config.dry_run {
                let _ = writeln!(io::stderr(), "patching file {}", target_path.display());
            }
            let prog = std::env::args().next().unwrap_or_else(|| "patch".to_string());
            let mtext = fp.malformed_text.as_deref().unwrap_or("");
            let _ = writeln!(io::stderr(), "{}: **** malformed patch at line {}: {}", prog, mline, mtext);
            let _ = writeln!(io::stderr());
            return Err(anyhow!("__malformed_reported__"));
        }

        if let Some(mode) = config.read_only {
            if let Ok(meta) = fs::metadata(&target_path) {
                if meta.permissions().readonly() {
                    match mode {
                        ReadOnlyMode::Ignore => {
                            readonly_forced_write = true;
                        }
                        ReadOnlyMode::Warn => {
                            let _ = writeln!(
                                io::stderr(),
                                "File {} is read-only; trying to patch anyway",
                                target_path.display()
                            );
                            readonly_forced_write = true;
                        }
                        ReadOnlyMode::Fail => {
                            some_failed = true;
                            let _ = writeln!(
                                io::stderr(),
                                "File {} is read-only; refusing to patch",
                                target_path.display()
                            );
                            if !config.dry_run {
                                let rej_path = if let Some(ref r) = config.reject_file {
                                    r.clone()
                                } else {
                                    reject_path_for(&target_path)
                                };
                                let append = written_rejects.contains(&rej_path);
                                let _ = write_reject_hunks(
                                    config,
                                    &rej_path,
                                    append,
                                    &target_path,
                                    fp.index_line.as_deref(),
                                    fp.source_format,
                                    fp.old_path.as_deref(),
                                    fp.new_path.as_deref(),
                                    fp.old_label.as_deref(),
                                    fp.new_label.as_deref(),
                                    &fail_hunks_for_readonly,
                                );
                                written_rejects.insert(rej_path.clone());
                                let _ = writeln!(
                                    io::stderr(),
                                    "1 out of 1 hunk ignored -- saving rejects to file {}",
                                    rej_path.display()
                                );
                            } else {
                                let _ = writeln!(io::stderr(), "1 out of 1 hunk ignored");
                            }
                            continue;
                        }
                    }
                }
            }
        }
        let effective_old_is_symlink = effective_old_mode == Some("120000");
        let effective_new_is_symlink = effective_new_mode == Some("120000");
        let break_symlink_for_write = config.follow_symlinks
            && fs::symlink_metadata(&target_path)
                .map(|m| m.file_type().is_symlink())
                .unwrap_or(false)
            && !effective_old_is_symlink
            && !effective_new_is_symlink;
        let hunks = if config.reverse {
            fp.hunks.iter().map(swap_hunk_for_reverse).collect::<Vec<_>>()
        } else {
            fp.hunks.iter().cloned().collect::<Vec<_>>()
        };

        if config.origfile.is_none() && is_potentially_dangerous_path(&target_path) {
            some_failed = true;
            let _ = writeln!(
                io::stderr(),
                "Ignoring potentially dangerous file name {}",
                quote_path(&target_path, config.quoting_style.as_deref())
            );
            if let (Some(old), Some(new)) = (fp.old_label.as_deref(), fp.new_label.as_deref()) {
                let _ = writeln!(io::stderr(), "can't find file to patch at input line 3");
                let _ = writeln!(io::stderr(), "Perhaps you used the wrong -p or --strip option?");
                let _ = writeln!(io::stderr(), "The text leading up to this was:");
                let _ = writeln!(io::stderr(), "--------------------------");
                let _ = writeln!(io::stderr(), "|--- {}", old);
                let _ = writeln!(io::stderr(), "|+++ {}", new);
                let _ = writeln!(io::stderr(), "--------------------------");
            }
            let _ = writeln!(io::stderr(), "No file to patch.  Skipping patch.");
            let _ = writeln!(io::stderr(), "1 out of 1 hunk ignored");
            continue;
        }

        if config.origfile.is_none() && is_invalid_target_name(&target_path) {
            some_failed = true;
            let _ = writeln!(io::stderr(), "Invalid file name {} -- skipping patch", target_path.display());
            continue;
        }

        if effective_old_is_symlink || effective_new_is_symlink {
            if !config.quiet {
                let _ = writeln!(io::stderr(), "patching symbolic link {}", target_path.display());
            }
            if config.dry_run {
                continue;
            }
            if config.backup {
                let _ = create_backup(
                    &target_path,
                    config,
                    false,
                    effective_old_is_devnull,
                );
            }
            if effective_new_is_symlink {
                let target = symlink_target_from_hunks(&hunks).unwrap_or_default();
                if target_path.exists() || fs::symlink_metadata(&target_path).is_ok() {
                    let _ = fs::remove_file(&target_path);
                }
                create_symlink_at(&target, &target_path)?;
            } else if effective_old_is_symlink {
                let _ = fs::remove_file(&target_path);
            }
            continue;
        }

        if !config.follow_symlinks {
            if let Ok(meta) = fs::symlink_metadata(&target_path) {
                if !meta.file_type().is_file() {
                    some_failed = true;
                    let _ = writeln!(
                        io::stderr(),
                        "File {} is not a regular file -- refusing to patch",
                        quote_path(&target_path, config.quoting_style.as_deref())
                    );
                    if !config.dry_run {
                        let rej = reject_path_for(&target_path);
                        let _ = writeln!(
                            io::stderr(),
                            "1 out of 1 hunk ignored -- saving rejects to file {}",
                            rej.display()
                        );
                    } else {
                        let _ = writeln!(io::stderr(), "1 out of 1 hunk ignored");
                    }
                    continue;
                }
            }
        }

        if source_path.is_none()
            && !source_for_read.exists()
            && !effective_old_is_devnull
            && !effective_new_is_devnull
            && patch_requires_existing_file(&fp.hunks)
        {
            some_failed = true;
            if let (Some(old), Some(new)) = (fp.old_label.as_deref(), fp.new_label.as_deref()) {
                let _ = writeln!(io::stderr(), "can't find file to patch at input line 3");
                let _ = writeln!(io::stderr(), "Perhaps you used the wrong -p or --strip option?");
                let _ = writeln!(io::stderr(), "The text leading up to this was:");
                let _ = writeln!(io::stderr(), "--------------------------");
                let _ = writeln!(io::stderr(), "|--- {}", old);
                let _ = writeln!(io::stderr(), "|+++ {}", new);
                let _ = writeln!(io::stderr(), "--------------------------");
            }
            let _ = writeln!(io::stderr(), "No file to patch.  Skipping patch.");
            let _ = writeln!(io::stderr(), "1 out of 1 hunk ignored");
            continue;
        }

        if effective_new_is_devnull && !source_for_read.exists() {
            some_failed = true;
            if config.force {
                let _ = writeln!(
                    io::stderr(),
                    "The next patch would delete the file {},",
                    target_path.display()
                );
                let _ = writeln!(io::stderr(), "which does not exist!  Applying it anyway.");
            } else {
                let _ = writeln!(
                    io::stderr(),
                    "The next patch would delete the file {},",
                    target_path.display()
                );
                let _ = writeln!(io::stderr(), "which does not exist!  Assume -R? [n] ");
                let _ = writeln!(io::stderr(), "Apply anyway? [n] ");
                let _ = writeln!(io::stderr(), "Skipping patch.");
                let _ = writeln!(io::stderr(), "1 out of 1 hunk ignored");
                continue;
            }
        }

        if effective_old_is_devnull && source_for_read.exists() && config.force {
            let _ = writeln!(
                io::stderr(),
                "The next patch would create the file {},",
                target_path.display()
            );
            let _ = writeln!(io::stderr(), "which already exists!  Applying it anyway.");
        }

        if let Some(ref bkp) = remembered_backup_path {
            if !bkp.exists() {
                if let Some(parent) = bkp.parent() {
                    let _ = fs::create_dir_all(parent);
                }
                let mut copied = false;
                if source_for_read.exists() {
                    #[cfg(unix)]
                    {
                        use std::os::unix::fs::MetadataExt;
                        if let Ok(meta) = fs::metadata(&source_for_read) {
                            if meta.nlink() > 1 {
                                if let Some(parent) = bkp.parent() {
                                    if let Ok(rd) = fs::read_dir(parent) {
                                        for ent in rd.flatten() {
                                            let p = ent.path();
                                            if p == *bkp {
                                                continue;
                                            }
                                            if p.file_name()
                                                .and_then(|s| s.to_str())
                                                .map(|s| is_remembered_backup_name(s, config))
                                                .unwrap_or(false)
                                                && p.is_file()
                                            {
                                                if fs::copy(&p, bkp).is_ok() {
                                                    copied = true;
                                                    break;
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                    if !copied {
                        let _ = fs::copy(&source_for_read, bkp);
                    }
                } else {
                    let _ = fs::File::create(bkp);
                }
            }
        }

        let mut deferred_patching_banner = false;
        let may_advise_empty_out = source_for_read.exists()
            && fp.hunks.iter().all(|h| h.new_len == 0)
            && (config.force || (config.batch && !config.force))
            && fs::metadata(&source_for_read).map(|m| m.len() == 0).unwrap_or(false);

        if !config.quiet && !config.verbose {
            if config.dry_run {
                let _ = writeln!(io::stderr(), "checking file {}", target_path.display());
            } else {
                if may_advise_empty_out {
                    deferred_patching_banner = true;
                }
                match op_desc {
                    Some("rename") => {
                        if !deferred_patching_banner {
                            let from = source_path.as_ref().map(|p| p.display().to_string()).unwrap_or_default();
                            if source_path.as_ref().map(|p| p.exists()).unwrap_or(false) {
                                let _ = writeln!(io::stderr(), "patching file {} (renamed from {})", target_path.display(), from);
                            } else {
                                let _ = writeln!(io::stderr(), "patching file {} (already renamed from {})", target_path.display(), from);
                            }
                        }
                    }
                    Some("copy") => {
                        if !deferred_patching_banner {
                            let from = source_path.as_ref().map(|p| p.display().to_string()).unwrap_or_default();
                            let _ = writeln!(io::stderr(), "patching file {} (copied from {})", target_path.display(), from);
                        }
                    }
                    _ => {
                        if !deferred_patching_banner {
                            let _ = writeln!(io::stderr(), "patching file {}", target_path.display());
                        }
                    }
                }
            }
        }

        // Prereq：仅对第一个文件检查；若目标文件内容不包含 Prereq 则跳过该块。
        if let Some(ref req) = prereq {
            if !prereq_checked {
                prereq_checked = true;
                match read_lines(&target_path, config.follow_symlinks, config.binary) {
                    Ok(content) => {
                        let full = content.join("");
                        if !full.contains(req) {
                            if config.verbose {
                                let _ = writeln!(
                                    io::stderr(),
                                    "patch: 跳过 {}（不满足 Prereq）",
                                    quote_path(&target_path, config.quoting_style.as_deref())
                                );
                            }
                            continue;
                        }
                    }
                    Err(_) => {
                        if config.verbose {
                            let _ = writeln!(
                                io::stderr(),
                                "patch: 跳过 {}（无法读取以检查 Prereq）",
                                quote_path(&target_path, config.quoting_style.as_deref())
                            );
                        }
                        continue;
                    }
                }
            }
        }

        if config.patch_get.is_some() && config.patch_get.unwrap() > 0 {
            try_version_get(&target_path)?;
        }

        if target_path.exists() && !source_snapshot.contains_key(&target_path) {
            let snap = read_lines(&target_path, config.follow_symlinks, config.binary)
                .unwrap_or_else(|_| Vec::new());
            source_snapshot.insert(target_path.clone(), snap);
        }

        let is_rename_or_copy = op_desc == Some("rename") || op_desc == Some("copy");
        if is_rename_or_copy {
            if let Some(src) = source_path.as_ref() {
                if src.exists() && !source_snapshot.contains_key(src) {
                    let snap = read_lines(src, config.follow_symlinks, config.binary)
                        .unwrap_or_else(|_| Vec::new());
                    source_snapshot.insert(src.clone(), snap);
                }
            }
        }

        let entry = file_cache.entry(target_path.clone()).or_insert_with(|| {
            if is_rename_or_copy {
                if let Some(src) = source_path.as_ref() {
                    if let Some(snap) = source_snapshot.get(src) {
                        return snap.clone();
                    }
                }
            }
            read_lines(&source_for_read, config.follow_symlinks, config.binary)
                .unwrap_or_else(|_| Vec::new())
        });

        let create_conflict = effective_old_is_devnull
            && source_for_read.exists()
            && !entry.is_empty();
        let patch_expected_empty = !fp.hunks.is_empty() && fp.hunks.iter().all(|h| h.new_len == 0);

        if source_for_read.exists() && entry.is_empty() && patch_expected_empty {
            if config.batch && !config.force {
                let _ = writeln!(
                    io::stderr(),
                    "The next patch would empty out the file {},",
                    target_path.display()
                );
                let _ = writeln!(
                    io::stderr(),
                    "which is already empty!  Assuming -R."
                );
            } else if config.force {
                let _ = writeln!(
                    io::stderr(),
                    "The next patch would empty out the file {},",
                    target_path.display()
                );
                let _ = writeln!(
                    io::stderr(),
                    "which is already empty!  Applying it anyway."
                );
            }
            if deferred_patching_banner && !config.quiet && !config.dry_run {
                let _ = writeln!(io::stderr(), "patching file {}", target_path.display());
            }
        }

        if fp.hunks.is_empty()
            && (effective_new_is_devnull || effective_new_mode == Some("000000"))
            && !effective_old_is_devnull
        {
            let can_delete_empty = source_for_read.exists()
                && fs::metadata(&source_for_read).map(|m| m.len() == 0).unwrap_or(false);
            if can_delete_empty {
                if !config.dry_run {
                    let _ = create_backup(&target_path, config, false, false).map_err(|e| {
                        some_failed = true;
                        let _ = writeln!(
                            io::stderr(),
                            "patch: 备份失败 {}: {}",
                            quote_path(&target_path, config.quoting_style.as_deref()),
                            e
                        );
                    });
                    let _ = fs::remove_file(&target_path);
                }
                continue;
            }
            if !config.dry_run {
                let _ = create_backup(&target_path, config, false, false).map_err(|e| {
                    some_failed = true;
                    let _ = writeln!(
                        io::stderr(),
                        "patch: 备份失败 {}: {}",
                        quote_path(&target_path, config.quoting_style.as_deref()),
                        e
                    );
                });
            }
            some_failed = true;
            let _ = writeln!(
                io::stderr(),
                "Not deleting file {} as content differs from patch",
                target_path.display()
            );
            continue;
        }

        let fuzz = config.fuzz.unwrap_or(2);
        let (apply_ok, applied_with_mismatch) = if config.merge {
            apply_hunks_merge(entry, &hunks, fuzz, config.ignore_whitespace, config.binary, &target_path, config)
        } else if create_conflict {
            (false, false)
        } else {
            // 注意：apply 过程可能部分修改 entry；因此所有“失败重试”都必须基于原始文件内容的副本来做。
            let base = entry.clone();

            let mut trial = base.clone();
            let report = apply_hunks_with_report(
                &mut trial,
                &hunks,
                fuzz,
                config.ifdef_name.as_deref(),
                config.ignore_whitespace,
                config.binary,
            );
            let mut ok = report.iter().all(|r| r.success);
            let mut applied_with_mismatch = false;
            let mut any_success = report.iter().any(|r| r.success);

            if ok {
                for r in &report {
                    if r.success {
                        if !config.quiet && (config.verbose || r.offset != 0 || r.fuzz_used > 0) {
                            let mut msg = format!("Hunk #{} succeeded at {}", r.index, r.at_line);
                            if r.fuzz_used > 0 {
                                msg.push_str(&format!(" with fuzz {}", r.fuzz_used));
                            }
                            if r.offset != 0 {
                                let plural = if r.offset == 1 { "line" } else { "lines" };
                                msg.push_str(&format!(" (offset {} {}).", r.offset, plural));
                            } else {
                                msg.push('.');
                            }
                            if config.verbose {
                                let _ = writeln!(io::stdout(), "{}", msg);
                            } else {
                                let _ = writeln!(io::stderr(), "{}", msg);
                            }
                        }
                    }
                }
            }

            // 自动探测反向：失败后尝试反向；仅在 --batch 下自动采用。
            if !ok && !config.reverse_specified && !config.noreverse {
                let rev_hunks: Vec<Hunk> = fp.hunks.iter().map(swap_hunk_for_reverse).collect();
                let mut fresh = base.clone();
                if apply_hunks_with_fuzz(
                    &mut fresh,
                    &rev_hunks,
                    fuzz,
                    config.ifdef_name.as_deref(),
                    config.ignore_whitespace,
                    config.binary,
                    true,
                )
                .is_ok()
                {
                    if config.batch {
                        trial = fresh;
                        ok = true;
                        applied_with_mismatch = true;
                        any_success = true;
                    } else {
                        // 交互式提示：询问用户是否继续
                        if !config.quiet {
                            let _ = writeln!(
                                io::stderr(),
                                "Reversed (or previously applied) patch detected!  Assume -R? [n] "
                            );
                            let _ = writeln!(io::stderr(), "Apply anyway? [n] ");
                            let _ = io::stderr().flush();
                            let mut input = String::new();
                            let _ = io::stdin().read_line(&mut input);
                            let answer = input.trim().to_lowercase();
                            if answer == "y" || answer == "yes" {
                                // 用户选择继续应用
                                trial = fresh;
                                ok = true;
                                applied_with_mismatch = true;
                                any_success = true;
                            } else {
                                // 用户选择不应用，跳过该补丁
                                some_failed = true;
                                if !config.quiet {
                                    let _ = writeln!(io::stderr(), "Skipping patch.");
                                }
                                if !config.dry_run {
                                    let rej_path = if let Some(ref r) = config.reject_file {
                                        r.clone()
                                    } else {
                                        reject_path_for(&target_path)
                                    };
                                    let append = written_rejects.contains(&rej_path);
                                    let failed_hunks: Vec<Hunk> = fp.hunks.iter().cloned().collect();
                                    let _ = write_reject_hunks(
                                        config,
                                        &rej_path,
                                        append,
                                        &target_path,
                                        fp.index_line.as_deref(),
                                        fp.source_format,
                                        fp.old_path.as_deref(),
                                        fp.new_path.as_deref(),
                                        fp.old_label.as_deref(),
                                        fp.new_label.as_deref(),
                                        &failed_hunks,
                                    );
                                    written_rejects.insert(rej_path.clone());
                                    if !config.quiet {
                                        let _ = writeln!(
                                            io::stderr(),
                                            "{} out of {} {} FAILED -- saving rejects to file {}",
                                            failed_hunks.len().max(1),
                                            hunks.len().max(1),
                                            hunk_word(hunks.len().max(1)),
                                            rej_path.display()
                                        );
                                    }
                                } else if !config.quiet {
                                    let _ = writeln!(
                                        io::stderr(),
                                        "{} out of {} {} FAILED",
                                        hunks.len().max(1),
                                        hunks.len().max(1),
                                        hunk_word(hunks.len().max(1))
                                    );
                                }
                                continue;
                            }
                        } else {
                            // quiet 模式下，跳过该补丁
                            if config.verbose {
                                let _ = writeln!(
                                    io::stderr(),
                                    "patch: 跳过 {}（补丁似已应用或为反向）",
                                    quote_path(&target_path, config.quoting_style.as_deref())
                                );
                            }
                            continue;
                        }
                    }
                }
            }

            // 显式 -R 时不再自动回退到正向应用；保持与 GNU patch 的失败语义一致。

            if any_success {
                *entry = trial;
            }

            if !ok {
                // 使用逐 hunk 结果覆盖原有“只报第一个失败 hunk”的行为。
                let failed: Vec<HunkApplyReport> = report
                    .iter()
                    .filter(|r| !r.success)
                    .cloned()
                    .collect();

                if !config.dry_run {
                    let _ = create_backup(&target_path, config, false, false);
                }
                if !config.merge && config.noreverse {
                    some_failed = true;
                    if !config.quiet {
                        let _ = writeln!(
                            io::stderr(),
                            "Reversed (or previously applied) patch detected!  Skipping patch."
                        );
                    }
                    if !config.dry_run {
                        let rej_path = if let Some(ref r) = config.reject_file {
                            r.clone()
                        } else {
                            reject_path_for(&target_path)
                        };
                        let append = written_rejects.contains(&rej_path);
                        let failed_hunks: Vec<Hunk> = fp.hunks.iter().cloned().collect();
                        let _ = write_reject_hunks(
                            config,
                            &rej_path,
                            append,
                            &target_path,
                            fp.index_line.as_deref(),
                            fp.source_format,
                            fp.old_path.as_deref(),
                            fp.new_path.as_deref(),
                            fp.old_label.as_deref(),
                            fp.new_label.as_deref(),
                            &failed_hunks,
                        );
                        written_rejects.insert(rej_path.clone());
                        if !config.quiet {
                            let _ = writeln!(
                                io::stderr(),
                                "{} out of {} {} FAILED -- saving rejects to file {}",
                                failed_hunks.len().max(1),
                                hunks.len().max(1),
                                hunk_word(hunks.len().max(1)),
                                rej_path.display()
                            );
                        }
                    } else if !config.quiet {
                        let _ = writeln!(
                            io::stderr(),
                            "{} out of {} {} FAILED",
                            hunks.len().max(1),
                            hunks.len().max(1),
                            hunk_word(hunks.len().max(1))
                        );
                    }
                    (false, applied_with_mismatch)
                } else {
                    some_failed = true;
                    if !config.merge {
                        let orig_for_fail = read_lines(
                            &target_path,
                            config.follow_symlinks,
                            config.binary,
                        )
                        .unwrap_or_default();
                        let patch_has_crlf = hunks
                            .iter()
                            .flat_map(|h| h.lines.iter())
                            .any(|hl| hl.text.ends_with("\r\n"));
                        let target_has_crlf = orig_for_fail.iter().any(|l| l.ends_with("\r\n"));

                        for r in &report {
                            if r.success {
                                if !config.quiet && (config.verbose || r.offset != 0 || r.fuzz_used > 0) {
                                    let mut msg = format!("Hunk #{} succeeded at {}", r.index, r.at_line);
                                    if r.fuzz_used > 0 {
                                        msg.push_str(&format!(" with fuzz {}", r.fuzz_used));
                                    }
                                    if r.offset != 0 {
                                        let plural = if r.offset == 1 { "line" } else { "lines" };
                                        msg.push_str(&format!(" (offset {} {}).", r.offset, plural));
                                    } else {
                                        msg.push('.');
                                    }
                                    if config.verbose {
                                        let _ = writeln!(io::stdout(), "{}", msg);
                                    } else {
                                        let _ = writeln!(io::stderr(), "{}", msg);
                                    }
                                }
                            } else if config.binary && patch_has_crlf && !target_has_crlf {
                                if config.verbose {
                                    let _ = writeln!(
                                        io::stdout(),
                                        "Hunk #{} FAILED at {} (different line endings).",
                                        r.index,
                                        r.at_line
                                    );
                                } else {
                                    let _ = writeln!(
                                        io::stderr(),
                                        "Hunk #{} FAILED at {} (different line endings).",
                                        r.index,
                                        r.at_line
                                    );
                                }
                            } else {
                                if config.verbose {
                                    let _ = writeln!(
                                        io::stdout(),
                                        "Hunk #{} FAILED at {}.",
                                        r.index,
                                        r.at_line
                                    );
                                } else {
                                    let _ = writeln!(
                                        io::stderr(),
                                        "Hunk #{} FAILED at {}.",
                                        r.index,
                                        r.at_line
                                    );
                                }
                            }
                        }

                        if (config.set_time || config.set_utc)
                            && fp.timestamp_header.is_some()
                            && !failed.is_empty()
                        {
                            let _ = writeln!(
                                io::stderr(),
                                "Not setting time of file {} (time mismatch)",
                                target_path.display()
                            );
                        }

                        if !config.dry_run {
                            let rej_path = if let Some(ref r) = config.reject_file {
                                r.clone()
                            } else {
                                reject_path_for(&target_path)
                            };
                            let append = written_rejects.contains(&rej_path);
                            let failed_hunks: Vec<Hunk> = failed.iter().map(|r| r.hunk.clone()).collect();
                            let _ = write_reject_hunks(
                                config,
                                &rej_path,
                                append,
                                &target_path,
                                fp.index_line.as_deref(),
                                fp.source_format,
                                fp.old_path.as_deref(),
                                fp.new_path.as_deref(),
                                fp.old_label.as_deref(),
                                fp.new_label.as_deref(),
                                &failed_hunks,
                            );
                            written_rejects.insert(rej_path.clone());
                            if !config.verbose {
                                let _ = writeln!(
                                    io::stderr(),
                                    "{} out of {} {} FAILED -- saving rejects to file {}",
                                    failed.len().max(1),
                                    hunks.len().max(1),
                                    hunk_word(hunks.len().max(1)),
                                    rej_path.display()
                                );
                            }
                        } else if !config.verbose {
                            let _ = writeln!(
                                io::stderr(),
                                "{} out of {} {} FAILED",
                                failed.len().max(1),
                                hunks.len().max(1),
                                hunk_word(hunks.len().max(1))
                            );
                        }
                    }
                    if !any_success {
                        (false, applied_with_mismatch)
                    } else {
                        (true, applied_with_mismatch)
                    }
                }
            } else {
                (ok, applied_with_mismatch)
            }
        };

        if !apply_ok {
            // --merge 模式下，即使有冲突，冲突标记已经插入到 entry 中，应该继续写文件
            if config.merge {
                // 冲突标记已插入，继续写文件流程
            } else if create_conflict {
                some_failed = true;
                let fail_at = hunks.first().map(|h| h.old_start.max(1)).unwrap_or(1);
                let _ = writeln!(io::stderr(), "Hunk #1 FAILED at {}.", fail_at);
                if !config.dry_run {
                    let rej_path = if let Some(ref r) = config.reject_file {
                        r.clone()
                    } else {
                        reject_path_for(&target_path)
                    };
                    let append = written_rejects.contains(&rej_path);
                    let _ = write_reject_hunks(
                        config,
                        &rej_path,
                        append,
                        &target_path,
                        fp.index_line.as_deref(),
                        fp.source_format,
                        fp.old_path.as_deref(),
                        fp.new_path.as_deref(),
                        fp.old_label.as_deref(),
                        fp.new_label.as_deref(),
                        &hunks,
                    );
                    written_rejects.insert(rej_path.clone());
                    let _ = writeln!(
                        io::stderr(),
                        "1 out of 1 hunk FAILED -- saving rejects to file {}",
                        rej_path.display()
                    );
                } else {
                    let _ = writeln!(io::stderr(), "1 out of 1 hunk FAILED");
                }
                continue;
            } else {
                continue;
            }
        }

        if config.dry_run {
            continue;
        }

        let out_path = config.output.as_ref().unwrap_or(&target_path);
        let out_path_preexisting = out_path.exists();
        let create_backup_when_missing = effective_old_is_devnull || !out_path_preexisting;
        let preserve = if effective_new_mode.is_some() {
            None
        } else {
            out_path
                .exists()
                .then(|| fs::metadata(out_path).ok())
                .flatten()
                .map(|m| m.permissions())
        };
        let backup_src = if op_desc == Some("rename") {
            source_path.as_ref().filter(|p| p.exists()).unwrap_or(out_path)
        } else {
            out_path
        };
        let _ = create_backup(
            backup_src,
            config,
            applied_with_mismatch,
            create_backup_when_missing,
        )
        .map_err(|e| {
            some_failed = true;
            let _ = writeln!(
                io::stderr(),
                "patch: 备份失败 {}: {}",
                quote_path(backup_src, config.quoting_style.as_deref()),
                e
            );
        });
        if (op_desc == Some("rename") || op_desc == Some("copy")) && backup_src != out_path {
            let _ = create_backup(out_path, config, applied_with_mismatch, true).map_err(|e| {
                some_failed = true;
                let _ = writeln!(
                    io::stderr(),
                    "patch: 备份失败 {}: {}",
                    quote_path(out_path, config.quoting_style.as_deref()),
                    e
                );
            });
        }
        if op_desc == Some("copy") && !out_path.exists() {
            let _ = create_backup(out_path, config, applied_with_mismatch, true).map_err(|e| {
                some_failed = true;
                let _ = writeln!(
                    io::stderr(),
                    "patch: 备份失败 {}: {}",
                    quote_path(out_path, config.quoting_style.as_deref()),
                    e
                );
            });
        }
        if let Some(parent) = out_path.parent() {
            let _ = fs::create_dir_all(parent);
        }
        if break_symlink_for_write {
            let _ = fs::remove_file(out_path);
        }
        if readonly_forced_write && out_path.exists() {
            let _ = force_make_writable(out_path);
        }
        if let Err(e) = write_lines(out_path, entry, config.binary) {
            some_failed = true;
            let _ = writeln!(io::stderr(), "patch: 写出失败 {}: {}", out_path.display(), e);
        } else {
            if op_desc == Some("rename") {
                if let Some(src) = source_path.as_ref() {
                    let rename_collision = fp.hunks.is_empty()
                        && src.exists()
                        && out_path_preexisting
                        && src != out_path;
                    if src.exists() && src != out_path && !rename_collision {
                        let _ = fs::remove_file(src);
                    }
                }
            }
            set_times_and_mode(out_path, config, preserve, fp.timestamp_header.as_deref())?;
            if let Some(mode) = effective_new_mode {
                if mode != "120000" && mode != "000000" {
                    let mode_changed = effective_old_mode != effective_new_mode;
                    if mode_changed || effective_old_is_devnull || !source_for_read.exists() {
                        let _ = apply_git_mode(out_path, mode);
                    }
                }
            }
            if patch_expected_empty && !entry.is_empty() && !applied_with_mismatch {
                some_failed = true;
                let _ = writeln!(
                    io::stderr(),
                    "Not deleting file {} as content differs from patch",
                    out_path.display()
                );
            }
            let should_delete_by_patch =
                effective_new_is_devnull && entry.is_empty() && !config.posix;
            if should_delete_by_patch && out_path.exists() {
                if fs::remove_file(out_path).is_ok() && config.remove_empty_files {
                    let _ = remove_empty_parent_dirs(out_path);
                }
            }
            if config.remove_empty_files && entry.is_empty() && out_path.exists() {
                match fs::remove_file(out_path) {
                    Ok(_) => {
                        let _ = remove_empty_parent_dirs(out_path);
                    }
                    Err(e) => {
                        let _ = writeln!(
                            io::stderr(),
                            "patch: 删除空文件失败 {}: {}",
                            quote_path(out_path, config.quoting_style.as_deref()),
                            e
                        );
                    }
                }
            }
        }
    }
    Ok(SomeFailedFlag { some_failed })
}

/// merge 模式下每个 hunk 的应用结果。
#[derive(Debug, Clone)]
struct MergeHunkReport {
    index: usize,
    success: bool,
    at_line: usize,
    /// 如果不成功，冲突标记覆盖的行范围（在结果文件中）。
    conflict_start: usize,
    conflict_end: usize,
}

fn apply_hunks_merge(
    target_lines: &mut Vec<String>,
    hunks: &[Hunk],
    fuzz: i32,
    ignore_whitespace: bool,
    binary: bool,
    _target_path: &Path,
    config: &PatchConfig,
) -> (bool, bool) {
    let mut offset: isize = 0;
    let mut some_failed = false;
    let mut some_success = false;
    let mut merged_at = Vec::new();
    let mut not_merged_at = Vec::new();

    for (_idx, h) in hunks.iter().enumerate() {
        let base = if h.old_start == 0 { 0 } else { h.old_start as isize - 1 };
        let guess = base + offset;
        let start = if guess >= 0 {
            locate_hunk_position(target_lines, h, guess as usize, fuzz, ignore_whitespace, binary).ok()
        } else {
            None
        };
        match start {
            Some(s) => {
                // 尝试应用 hunk
                let apply_result = apply_single_hunk(target_lines, h, s, binary);
                if apply_result.is_ok() {
                    offset += h.new_len as isize - h.old_len as isize;
                    some_success = true;
                    merged_at.push(s + 1);
                } else {
                    // 应用失败，在 merge 模式下生成冲突标记
                    let insert_pos = if h.old_start == 0 {
                        0
                    } else {
                        ((h.old_start as isize - 1) + offset).max(0) as usize
                    };
                    if let Some((cs, ce)) = insert_conflict_markers_merge(target_lines, h, insert_pos, ignore_whitespace, binary) {
                        not_merged_at.push((cs, ce));
                    }
                    let new_len = target_lines.len();
                    offset += h.new_len as isize - h.old_len as isize;
                    some_failed = true;
                }
            }
            None => {
                // 在 merge 模式下，当 hunk 整体匹配失败时，逐行处理生成冲突标记
                let insert_pos = if h.old_start == 0 {
                    0
                } else {
                    ((h.old_start as isize - 1) + offset).max(0) as usize
                };
                if let Some((cs, ce)) = insert_conflict_markers_merge(target_lines, h, insert_pos, ignore_whitespace, binary) {
                    not_merged_at.push((cs, ce));
                }
                let new_len = target_lines.len();
                offset += h.new_len as isize - h.old_len as isize;
                some_failed = true;
            }
        }
    }

    // 输出类似 GNU patch 的消息
    if !config.quiet && (some_success || some_failed) {
        if some_success && some_failed {
            let merged_str: Vec<String> = merged_at.iter().map(|l| format!("{}", l)).collect();
            let not_merged_str: Vec<String> = not_merged_at.iter().map(|(s, e)| {
                if s == e { format!("{}", s) } else { format!("{}-{}", s, e) }
            }).collect();
            let _ = writeln!(
                io::stderr(),
                "Hunk #1 merged at {}, NOT MERGED at {}.",
                merged_str.join(", "),
                not_merged_str.join(", ")
            );
        } else if some_failed {
            let not_merged_str: Vec<String> = not_merged_at.iter().map(|(s, e)| {
                if s == e { format!("{}", s) } else { format!("{}-{}", s, e) }
            }).collect();
            let _ = writeln!(
                io::stderr(),
                "Hunk #1 NOT MERGED at {}.",
                not_merged_str.join(", ")
            );
        }
    }

    (!some_failed, false)
}

/// 在 merge 模式下插入冲突标记，返回冲突标记在结果文件中的行范围（1-based，闭区间）。
/// 如果没有冲突标记，返回 None。
fn insert_conflict_markers_merge(
    target_lines: &mut Vec<String>,
    hunk: &Hunk,
    start: usize,
    ignore_whitespace: bool,
    binary: bool,
) -> Option<(usize, usize)> {
    let mut result = Vec::new();
    let mut target_idx = start;
    let mut in_conflict = false;
    let mut conflict_old = Vec::new();
    let mut conflict_new = Vec::new();
    // 记录每个冲突块的起始和结束位置（在 result 中，0-based）
    let mut conflict_blocks: Vec<(usize, usize)> = Vec::new();
    let mut conflict_block_start: Option<usize> = None;

    // 预处理 hunk 行，将 Remove-Add 对分组
    let mut grouped: Vec<HunkGroup> = Vec::new();
    let mut i = 0;
    while i < hunk.lines.len() {
        match hunk.lines[i].kind {
            HunkLineKind::Context => {
                grouped.push(HunkGroup::Context(&hunk.lines[i]));
                i += 1;
            }
            HunkLineKind::Remove => {
                let remove_line = &hunk.lines[i];
                i += 1;
                let add_line = if i < hunk.lines.len() && hunk.lines[i].kind == HunkLineKind::Add {
                    let al = &hunk.lines[i];
                    i += 1;
                    Some(al)
                } else {
                    None
                };
                grouped.push(HunkGroup::Change { remove: remove_line, add: add_line });
            }
            HunkLineKind::Add => {
                grouped.push(HunkGroup::PureAdd(&hunk.lines[i]));
                i += 1;
            }
        }
    }

    for g in &grouped {
        match g {
            HunkGroup::Context(hl) => {
                if in_conflict {
                    in_conflict = false;
                    let block_start = result.len();
                    result.push("<<<<<<<\n".to_string());
                    result.extend(conflict_old.drain(..));
                    result.push("=======\n".to_string());
                    result.extend(conflict_new.drain(..));
                    result.push(">>>>>>>\n".to_string());
                    let block_end = result.len() - 1;
                    conflict_blocks.push((block_start, block_end));
                }
                if target_idx < target_lines.len() {
                    result.push(target_lines[target_idx].clone());
                    target_idx += 1;
                }
            }
            HunkGroup::Change { remove, add } => {
                let matches = if target_idx < target_lines.len() {
                    let target_line = &target_lines[target_idx];
                    if ignore_whitespace {
                        normalize_blanks(&normalize_line_endings(&remove.text, binary))
                            == normalize_blanks(&normalize_line_endings(target_line, binary))
                    } else {
                        normalize_line_endings(&remove.text, binary) == normalize_line_endings(target_line, binary)
                    }
                } else {
                    false
                };

                if matches {
                    if in_conflict {
                        in_conflict = false;
                        let block_start = result.len();
                        result.push("<<<<<<<\n".to_string());
                        result.extend(conflict_old.drain(..));
                        result.push("=======\n".to_string());
                        result.extend(conflict_new.drain(..));
                        result.push(">>>>>>>\n".to_string());
                        let block_end = result.len() - 1;
                        conflict_blocks.push((block_start, block_end));
                    }
                    if let Some(a) = add {
                        result.push(a.text.clone());
                    }
                    target_idx += 1;
                } else {
                    in_conflict = true;
                    if target_idx < target_lines.len() {
                        conflict_old.push(target_lines[target_idx].clone());
                        target_idx += 1;
                    }
                    if let Some(a) = add {
                        conflict_new.push(a.text.clone());
                    }
                }
            }
            HunkGroup::PureAdd(hl) => {
                if !in_conflict {
                    in_conflict = true;
                }
                conflict_new.push(hl.text.clone());
            }
        }
    }

    if in_conflict {
        let block_start = result.len();
        result.push("<<<<<<<\n".to_string());
        result.extend(conflict_old.drain(..));
        result.push("=======\n".to_string());
        result.extend(conflict_new.drain(..));
        result.push(">>>>>>>\n".to_string());
        let block_end = result.len() - 1;
        conflict_blocks.push((block_start, block_end));
    }

    let end = target_idx.min(target_lines.len());
    let result_len = result.len();
    target_lines.splice(start..end, result);

    // 将 conflict_blocks 中的 0-based 位置转换为最终文件中的 1-based 位置
    if conflict_blocks.is_empty() {
        None
    } else {
        // 返回第一个冲突块的起始和最后一个冲突块的结束（1-based）
        let first_start = conflict_blocks[0].0 + 1;
        let last_end = conflict_blocks.last().unwrap().1 + 1;
        Some((first_start, last_end))
    }
}

enum HunkGroup<'a> {
    Context(&'a HunkLine),
    Change { remove: &'a HunkLine, add: Option<&'a HunkLine> },
    PureAdd(&'a HunkLine),
}

fn try_version_get(path: &Path) -> Result<()> {
    let dir = path.parent().unwrap_or(Path::new("."));
    let base = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
    let rcs = dir.join("RCS").join(format!("{},v", base));
    if rcs.exists() {
        let status = std::process::Command::new("co").arg("-l").arg(path).status();
        if let Ok(s) = status {
            if !s.success() {
                let _ = writeln!(io::stderr(), "patch: co 失败，继续使用当前文件");
            }
        }
    }
    Ok(())
}

fn set_times_and_mode(
    path: &Path,
    config: &PatchConfig,
    preserve_permissions: Option<std::fs::Permissions>,
    timestamp_header: Option<&str>,
) -> Result<()> {
    if config.set_time || config.set_utc {
        let now_ft = FileTime::from_system_time(SystemTime::now());
        let mtime = if let Some(ts) = timestamp_header {
            parse_timestamp_from_header(ts, config.set_utc).unwrap_or(now_ft)
        } else {
            now_ft
        };
        filetime::set_file_mtime(path, mtime)?;
    }
    if let Some(perm) = preserve_permissions {
        fs::set_permissions(path, perm)?;
    }
    Ok(())
}

/// 解析 diff 头中的时间戳（如 "2002-02-21 23:30:39.123 -0800" 或 "2002-02-21 23:30:39"）。
/// assume_utc：无时区时按 UTC 解释（-Z），否则按本地（-T）。
fn parse_timestamp_from_header(s: &str, assume_utc: bool) -> Option<FileTime> {
    let s = s.trim();
    if s.is_empty() {
        return None;
    }
    // 尝试带时区：末尾 " ±HHMM"（6 字符）
    let offset_secs = if s.len() >= 6 {
        let tail = s.as_bytes();
        let n = tail.len();
        let sign = if tail[n - 6] == b'+' { 1i32 } else if tail[n - 6] == b'-' { -1i32 } else { 0 };
        if sign != 0 && tail[n - 5].is_ascii_digit() && tail[n - 4].is_ascii_digit() && tail[n - 3].is_ascii_digit() && tail[n - 2].is_ascii_digit() {
            let h: i32 = (tail[n - 5] - b'0') as i32 * 10 + (tail[n - 4] - b'0') as i32;
            let m: i32 = (tail[n - 3] - b'0') as i32 * 10 + (tail[n - 2] - b'0') as i32;
            Some(sign * (h * 3600 + m * 60))
        } else {
            None
        }
    } else {
        None
    };
    let (naive_str, offset_secs) = if let Some(off) = offset_secs {
        let trim = s.len().saturating_sub(6);
        (s[..trim].trim_end(), Some(off))
    } else {
        (s, None)
    };
    // 解析 "YYYY-MM-DD HH:MM:SS" 或 "YYYY-MM-DD HH:MM:SS.frac"
    let dt = if naive_str.contains('.') {
        NaiveDateTime::parse_from_str(naive_str, "%Y-%m-%d %H:%M:%S%.f").ok()
    } else {
        NaiveDateTime::parse_from_str(naive_str, "%Y-%m-%d %H:%M:%S").ok()
    };
    let dt = dt.or_else(|| NaiveDateTime::parse_from_str(naive_str, "%Y-%m-%d %H:%M").ok())?;
    let unix = match offset_secs {
        Some(off) => {
            // 头中的时间为该时区的“本地时间”，UTC = 本地 - offset（off 为 east 秒数，-0800 => -28800）
            dt.and_utc().timestamp() - off as i64
        }
        None => {
            if assume_utc {
                dt.and_utc().timestamp()
            } else {
                dt.and_local_timezone(chrono::Local)
                    .single()
                    .map(|t| t.timestamp())
                    .unwrap_or_else(|| dt.and_utc().timestamp())
            }
        }
    };
    let subsec = dt.and_utc().timestamp_subsec_nanos();
    let (secs, subsec) = if unix >= 0 {
        (unix as u64, subsec)
    } else {
        (0, 0u32)
    };
    Some(FileTime::from_unix_time(secs as i64, subsec))
}

fn apply_ed_script<R: BufRead>(reader: &mut R, target: &Path, config: &PatchConfig) -> Result<()> {
    let mut lines = read_lines(target, config.follow_symlinks, config.binary)?;
    let mut buf = String::new();
    while reader.read_line(&mut buf)? != 0 {
        let cmd = buf.trim_end().to_string();
        buf.clear();
        if cmd.is_empty() {
            continue;
        }
        if cmd == "." {
            continue;
        }
        let (addr, rest) = parse_ed_addr(&cmd);
        if rest.is_empty() {
            continue;
        }
        let ch = rest.chars().next().unwrap();
        match ch {
            'a' => {
                let mut ins = Vec::new();
                while reader.read_line(&mut buf)? != 0 && buf.trim_end() != "." {
                    ins.push(buf.clone());
                    buf.clear();
                }
                buf.clear();
                let at = addr.0.min(lines.len());
                for (i, l) in ins.into_iter().enumerate() {
                    lines.insert(at + i, l);
                }
            }
            'd' => {
                let (start, count) = addr;
                let start = start.saturating_sub(1).min(lines.len());
                let end = (start + count).min(lines.len());
                if start < end {
                    lines.drain(start..end);
                }
            }
            'c' => {
                let (start, count) = addr;
                let mut ins = Vec::new();
                while reader.read_line(&mut buf)? != 0 && buf.trim_end() != "." {
                    ins.push(buf.clone());
                    buf.clear();
                }
                buf.clear();
                let start = start.saturating_sub(1).min(lines.len());
                let end = (start + count).min(lines.len());
                if start < end {
                    lines.drain(start..end);
                }
                for (i, l) in ins.into_iter().enumerate() {
                    lines.insert(start + i, l);
                }
            }
            _ => {}
        }
    }
    if !config.dry_run {
        let default_out = target.to_path_buf();
        let out = config.output.as_ref().unwrap_or(&default_out);
        write_lines(out, &lines, config.binary)?;
    }
    Ok(())
}

fn parse_ed_addr(cmd: &str) -> ((usize, usize), &str) {
    let mut i = 0;
    let bytes = cmd.as_bytes();
    while i < bytes.len() && bytes[i].is_ascii_digit() {
        i += 1;
    }
    let start: usize = std::str::from_utf8(&bytes[..i]).unwrap_or("0").parse().unwrap_or(0);
    let mut count = 1usize;
    if i < bytes.len() && bytes[i] == b',' {
        i += 1;
        let j = i;
        while i < bytes.len() && bytes[i].is_ascii_digit() {
            i += 1;
        }
        let end: usize = std::str::from_utf8(&bytes[j..i]).unwrap_or("0").parse().unwrap_or(0);
        count = end.saturating_sub(start).max(1);
    }
    ((start, count), cmd.get(i..).unwrap_or(""))
}

/// 自动检测补丁格式并解析。支持 unified 与 context diff。返回 (补丁列表, Prereq)。
fn parse_patches<R: BufRead>(reader: &mut R) -> Result<(Vec<FilePatch>, Option<String>)> {
    let mut peek = String::new();
    if reader.read_line(&mut peek)? == 0 {
        bail!("补丁输入为空");
    }
    let is_context = peek.starts_with("*** ");
    let mut combined = io::Read::chain(Cursor::new(peek), reader);
    if is_context {
        parse_context_patches(&mut combined)
    } else {
        parse_unified_patches(&mut combined)
    }
}

/// Normal diff: 1a2 / 3d2 / 5,7c8,9 等，后跟 "< " / "> " 行。
fn parse_normal_patches<R: BufRead>(reader: &mut R) -> Result<Vec<FilePatch>> {
    let mut hunks = Vec::new();
    let mut buf = String::new();
    while reader.read_line(&mut buf)? != 0 {
        let line = buf.trim_end().to_string();
        buf.clear();
        if line.is_empty() {
            continue;
        }
        let (old_start, old_len, new_start, new_len, cmd) = parse_normal_command(&line)?;
        let mut hunk_lines = Vec::new();
        match cmd {
            'a' => {
                for _ in 0..new_len {
                    buf.clear();
                    if reader.read_line(&mut buf)? == 0 {
                        break;
                    }
                    let l = buf.strip_suffix('\n').unwrap_or(&buf);
                    let text = if l.starts_with("> ") { l[2..].to_string() + "\n" } else { l.to_string() + "\n" };
                    hunk_lines.push(HunkLine { kind: HunkLineKind::Add, text });
                }
            }
            'd' => {
                for _ in 0..old_len {
                    buf.clear();
                    if reader.read_line(&mut buf)? == 0 {
                        break;
                    }
                    let l = buf.strip_suffix('\n').unwrap_or(&buf);
                    let text = if l.starts_with("< ") { l[2..].to_string() + "\n" } else { l.to_string() + "\n" };
                    hunk_lines.push(HunkLine { kind: HunkLineKind::Remove, text });
                }
            }
            'c' => {
                for _ in 0..old_len {
                    buf.clear();
                    if reader.read_line(&mut buf)? == 0 {
                        break;
                    }
                    let l = buf.strip_suffix('\n').unwrap_or(&buf);
                    if l == "---" {
                        break;
                    }
                    let text = if l.starts_with("< ") { l[2..].to_string() + "\n" } else { l.to_string() + "\n" };
                    hunk_lines.push(HunkLine { kind: HunkLineKind::Remove, text });
                }
                buf.clear();
                reader.read_line(&mut buf)?;
                buf.clear();
                for _ in 0..new_len {
                    buf.clear();
                    if reader.read_line(&mut buf)? == 0 {
                        break;
                    }
                    let l = buf.strip_suffix('\n').unwrap_or(&buf);
                    let text = if l.starts_with("> ") { l[2..].to_string() + "\n" } else { l.to_string() + "\n" };
                    hunk_lines.push(HunkLine { kind: HunkLineKind::Add, text });
                }
            }
            _ => continue,
        }
        hunks.push(Hunk {
            old_start,
            old_len,
            new_start,
            new_len,
            header_suffix: None,
            lines: hunk_lines,
        });
    }
    Ok(vec![FilePatch {
        old_path: None,
        new_path: None,
        old_label: None,
        new_label: None,
        index_line: None,
        source_format: Some(RejectFormat::Unified),
        rename_from: None,
        rename_to: None,
        copy_from: None,
        copy_to: None,
        old_mode: None,
        new_mode: None,
        hunks,
        timestamp_header: None,
        malformed_line: None,
        malformed_text: None,
        git_binary_patch: false,
    }])
}

fn parse_normal_command(line: &str) -> Result<(usize, usize, usize, usize, char)> {
    let mut i = 0;
    let bytes = line.as_bytes();
    while i < bytes.len() && bytes[i].is_ascii_digit() {
        i += 1;
    }
    let old_start: usize = std::str::from_utf8(&bytes[..i]).unwrap_or("0").parse().unwrap_or(0);
    let mut old_len = 1usize;
    if i < bytes.len() && bytes[i] == b',' {
        i += 1;
        let j = i;
        while i < bytes.len() && bytes[i].is_ascii_digit() {
            i += 1;
        }
        let e: usize = std::str::from_utf8(&bytes[j..i]).unwrap_or("0").parse().unwrap_or(0);
        old_len = e.saturating_sub(old_start).max(1);
    }
    if i >= bytes.len() {
        bail!("invalid normal diff command: {}", line);
    }
    let cmd = bytes[i] as char;
    i += 1;
    let mut new_start = old_start;
    let mut new_len = 1usize;
    if i < bytes.len() && bytes[i].is_ascii_digit() {
        let j = i;
        while i < bytes.len() && bytes[i].is_ascii_digit() {
            i += 1;
        }
        let first_num: usize = std::str::from_utf8(&bytes[j..i]).unwrap_or("0").parse().unwrap_or(0);
        if cmd == 'a' {
            old_len = 0;
            new_len = first_num;
            new_start = old_start + 1;
        } else {
            new_start = first_num;
        }
        if cmd != 'a' && i < bytes.len() && bytes[i] == b',' {
            i += 1;
            let j = i;
            while i < bytes.len() && bytes[i].is_ascii_digit() {
                i += 1;
            }
            let e: usize = std::str::from_utf8(&bytes[j..i]).unwrap_or("0").parse().unwrap_or(0);
            new_len = e.saturating_sub(new_start).saturating_add(1).max(1);
        }
    } else if cmd == 'a' {
        old_len = 0;
    }
    Ok((old_start, old_len, new_start, new_len, cmd))
}

/// Context diff：*** file / --- file / *************** / *** range **** / old lines / --- range ---- / new lines
fn parse_context_patches<R: BufRead>(
    reader: &mut R,
) -> Result<(Vec<FilePatch>, Option<String>)> {
    let mut patches = Vec::new();
    let mut current: Option<FilePatch> = None;
    let mut buf = String::new();
    let mut prereq: Option<String> = None;
    let mut line_no: usize = 0;

    while reader.read_line(&mut buf)? != 0 {
        line_no += 1;
        let line = buf.clone();
        buf.clear();
        let trim = line.trim_end();

        if trim.starts_with("Prereq: ") {
            prereq = Some(trim[8..].trim().to_string());
            continue;
        }
        if trim.starts_with("*** ") && !trim.starts_with("********") {
            if let Some(fp) = current.take() {
                if !fp.hunks.is_empty() {
                    patches.push(fp);
                }
            }
            let path = extract_path_from_header(&line);
            current = Some(FilePatch {
                old_path: Some(path),
                new_path: None,
                old_label: Some(extract_header_label(&line)),
                new_label: None,
                index_line: None,
                source_format: Some(RejectFormat::Context),
                rename_from: None,
                rename_to: None,
                copy_from: None,
                copy_to: None,
                old_mode: None,
                new_mode: None,
                hunks: Vec::new(),
                timestamp_header: None,
                malformed_line: None,
                malformed_text: None,
                git_binary_patch: false,
            });
            continue;
        }
        if trim.starts_with("--- ") && !trim.starts_with("----") {
            if let Some(fp) = current.as_mut() {
                fp.new_path = Some(extract_path_from_header(&line));
                fp.new_label = Some(extract_header_label(&line));
                fp.timestamp_header = extract_timestamp_from_header(&line);
            }
            continue;
        }
        if trim.starts_with("***************") {
            let suffix = trim
                .strip_prefix("***************")
                .map(|s| s.trim())
                .filter(|s| !s.is_empty())
                .map(|s| format!(" {}", s));
            if let Some(fp) = current.as_mut() {
                if let Some(h) = parse_one_context_hunk(reader, &mut buf, suffix, line_no + 1)? {
                    fp.hunks.push(h);
                }
            }
        }
    }
    if let Some(fp) = current.take() {
        if !fp.hunks.is_empty() {
            patches.push(fp);
        }
    }
    Ok((patches, prereq))
}

fn parse_one_context_hunk<R: BufRead>(
    reader: &mut R,
    buf: &mut String,
    hunk_suffix: Option<String>,
    hunk_header_line: usize,
) -> Result<Option<Hunk>> {
    buf.clear();
    if reader.read_line(buf)? == 0 {
        return Ok(None);
    }
    let line = buf.trim_end();
    if !line.starts_with("*** ") || line.starts_with("********") {
        return Ok(None);
    }
    let (old_start, old_len) = parse_context_range(line.trim_start_matches('*').trim())?;
    let mut old_lines: Vec<(bool, String)> = Vec::new();
    loop {
        buf.clear();
        if reader.read_line(buf)? == 0 {
            break;
        }
        let l = buf.strip_suffix('\n').unwrap_or(&buf);
        if l.starts_with("--- ") && !l.starts_with("----") {
            let (new_start, new_len) = parse_context_range(l.trim_start_matches('-').trim())?;
            let mut new_lines: Vec<(bool, String)> = Vec::new();
            loop {
                buf.clear();
                if reader.read_line(buf)? == 0 {
                    break;
                }
                let l = buf.strip_suffix('\n').unwrap_or(&buf);
                if l.is_empty() || l.starts_with("***") {
                    break;
                }
                let is_add = l.starts_with('+') || l.starts_with('!');
                // context diff 的每行格式通常是: "<tag><space><content>"，例如 "  line", "! line", "+ line"
                // 因此应优先去掉前两个字符（tag + 分隔空格）。
                let content = if l.len() >= 2
                    && (l.starts_with(' ') || l.starts_with('-') || l.starts_with('+') || l.starts_with('!'))
                    && l.as_bytes()[1] == b' '
                {
                    l[2..].to_string() + "\n"
                } else if l.starts_with(' ') || l.starts_with('-') || l.starts_with('+') || l.starts_with('!') {
                    l[1..].to_string() + "\n"
                } else {
                    l.to_string() + "\n"
                };
                new_lines.push((is_add, content));
            }
            let mut out = Vec::new();
            let n = old_lines.len().max(new_lines.len());
            for i in 0..n {
                let (o_remove, o_text) = old_lines.get(i).map(|(r, t)| (*r, t.clone())).unwrap_or((false, String::new()));
                let (n_add, n_text) = new_lines.get(i).map(|(a, t)| (*a, t.clone())).unwrap_or((false, String::new()));
                if o_remove && n_add {
                    out.push(HunkLine { kind: HunkLineKind::Remove, text: o_text });
                    out.push(HunkLine { kind: HunkLineKind::Add, text: n_text });
                } else if o_remove {
                    out.push(HunkLine { kind: HunkLineKind::Remove, text: o_text });
                } else if n_add {
                    out.push(HunkLine { kind: HunkLineKind::Add, text: n_text });
                } else if !o_text.is_empty() || !n_text.is_empty() {
                    out.push(HunkLine { kind: HunkLineKind::Context, text: if !o_text.is_empty() { o_text } else { n_text } });
                }
            }

            let parsed_old = out
                .iter()
                .filter(|hl| matches!(hl.kind, HunkLineKind::Context | HunkLineKind::Remove))
                .count();
            let parsed_new = out
                .iter()
                .filter(|hl| matches!(hl.kind, HunkLineKind::Context | HunkLineKind::Add))
                .count();
            if parsed_old != old_len || parsed_new != new_len {
                bail!(
                    "replacement text or line numbers mangled in hunk at line {}",
                    hunk_header_line
                );
            }

            return Ok(Some(Hunk {
                old_start,
                old_len,
                new_start,
                new_len,
                header_suffix: hunk_suffix,
                lines: out,
            }));
        }
        let is_remove = l.starts_with('-') || l.starts_with('!');
        let content = if l.len() >= 2
            && (l.starts_with(' ') || l.starts_with('-') || l.starts_with('!'))
            && l.as_bytes()[1] == b' '
        {
            l[2..].to_string() + "\n"
        } else if l.starts_with(' ') || l.starts_with('-') || l.starts_with('!') {
            l[1..].to_string() + "\n"
        } else {
            l.to_string() + "\n"
        };
        old_lines.push((is_remove, content));
    }
    Ok(None)
}

fn parse_context_range(s: &str) -> Result<(usize, usize)> {
    let s = s.trim();
    let part = s.split_whitespace().next().unwrap_or("0");
    let (a, b) = if part.contains(',') {
        let mut sp = part.split(',');
        let a: usize = sp.next().and_then(|x| x.parse().ok()).unwrap_or(0);
        let b: usize = sp.next().and_then(|x| x.parse().ok()).unwrap_or(0);
        (a, b.saturating_sub(a).saturating_add(1))
    } else {
        let a: usize = part.parse().unwrap_or(0);
        (a, if a == 0 { 0 } else { 1 })
    };
    Ok((a, if a == 0 { b } else { b.max(1) }))
}

fn is_remembered_backup_name(name: &str, config: &PatchConfig) -> bool {
    if name.ends_with(".orig") || name.ends_with(".dpkg-orig") {
        return true;
    }
    if let Some(suffix) = config.backup_suffix.as_deref() {
        if !suffix.is_empty() && name.ends_with(suffix) {
            return true;
        }
    }
    false
}

fn backup_path_for(path: &Path, config: &PatchConfig, backup_reason_mismatch: bool) -> Result<PathBuf> {
    let do_backup = if backup_reason_mismatch {
        config.backup_if_mismatch && !config.no_backup_if_mismatch
    } else {
        config.backup
    };
    if !do_backup {
        return Ok(PathBuf::new());
    }
    let typ = config.version_control.unwrap_or(BackupType::Simple);
    let suffix = if (config.backup_prefix.is_some() || config.basename_prefix.is_some())
        && config.backup_suffix.is_none()
    {
        ""
    } else {
        config.backup_suffix.as_deref().unwrap_or(".orig")
    };
    let prefix = config.backup_prefix.as_deref().unwrap_or("");
    let basename_prefix = config.basename_prefix.as_deref().unwrap_or("");
    let backup_path = if prefix.contains('/') {
        let full = PathBuf::from(prefix).join(path);
        let parent = full
            .parent()
            .filter(|p| !p.as_os_str().is_empty())
            .unwrap_or(Path::new("."));
        let name = full.file_name().and_then(|n| n.to_str()).unwrap_or("file");
        let base = format!("{}{}", basename_prefix, name);
        match typ {
            BackupType::Simple => parent.join(format!("{}{}", base, suffix)),
            BackupType::Numbered => {
                let n = next_numbered_backup(parent, &base)?;
                parent.join(format!("{}.~{}~", base, n))
            }
            BackupType::Existing => {
                let (has_numbered, n) = existing_numbered_backup(parent, &base)?;
                if has_numbered {
                    parent.join(format!("{}.~{}~", base, n))
                } else {
                    parent.join(format!("{}{}", base, suffix))
                }
            }
        }
    } else {
        let parent = path
            .parent()
            .filter(|p| !p.as_os_str().is_empty())
            .unwrap_or(Path::new("."));
        let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("file");
        let base = format!("{}{}", basename_prefix, name);
        match typ {
            BackupType::Simple => parent.join(format!("{}{}{}", prefix, base, suffix)),
            BackupType::Numbered => {
                let n = next_numbered_backup(parent, &format!("{}{}", prefix, base))?;
                parent.join(format!("{}{}.~{}~", prefix, base, n))
            }
            BackupType::Existing => {
                let (has_numbered, n) =
                    existing_numbered_backup(parent, &format!("{}{}", prefix, base))?;
                if has_numbered {
                    parent.join(format!("{}{}.~{}~", prefix, base, n))
                } else {
                    parent.join(format!("{}{}{}", prefix, base, suffix))
                }
            }
        }
    };
    Ok(backup_path)
}

fn create_backup(
    path: &Path,
    config: &PatchConfig,
    backup_reason_mismatch: bool,
    create_when_missing: bool,
) -> Result<()> {
    let backup_path = backup_path_for(path, config, backup_reason_mismatch)?;
    if backup_path.as_os_str().is_empty() {
        return Ok(());
    }
    if let Some(parent) = backup_path.parent() {
        let _ = fs::create_dir_all(parent);
    }
    let path_exists = path.exists() || fs::symlink_metadata(path).is_ok();
    if !path_exists {
        if !create_when_missing {
            return Ok(());
        }
        if backup_path.exists() || fs::symlink_metadata(&backup_path).is_ok() {
            return Ok(());
        }
        fs::File::create(&backup_path)?;
        return Ok(());
    }
    if let Ok(meta) = fs::symlink_metadata(path) {
        if meta.file_type().is_symlink() {
            let target = fs::read_link(path)?;
            if backup_path.exists() || fs::symlink_metadata(&backup_path).is_ok() {
                let _ = fs::remove_file(&backup_path);
            }
            create_symlink_at(&target.to_string_lossy(), &backup_path)?;
            return Ok(());
        }
    }
    fs::copy(path, &backup_path)?;
    Ok(())
}

/// 已有 numbered 备份时返回 (true, 下一个编号)，否则 (false, 1)。
fn existing_numbered_backup(parent: &Path, base: &str) -> Result<(bool, u32)> {
    let mut max_n: u32 = 0;
    let prefix = format!("{}.~", base);
    let suffix = "~";
    for e in fs::read_dir(parent)? {
        let e = e?;
        let name = e.file_name();
        let s = name.to_str().unwrap_or("");
        if s.starts_with(&prefix) && s.ends_with(suffix) && prefix.len() + suffix.len() <= s.len() {
            let mid = &s[prefix.len()..s.len() - suffix.len()];
            if let Ok(n) = mid.parse::<u32>() {
                max_n = max_n.max(n);
            }
        }
    }
    Ok((max_n > 0, max_n + 1))
}

fn next_numbered_backup(parent: &Path, base: &str) -> Result<u32> {
    let (_, n) = existing_numbered_backup(parent, base)?;
    Ok(n)
}

fn read_lines(path: &Path, follow_symlinks: bool, binary: bool) -> Result<Vec<String>> {
    let f = if follow_symlinks {
        fs::File::open(path)?
    } else {
        open_file_no_follow(path)?
    };
    let reader = BufReader::new(f);
    if binary {
        let mut reader = reader;
        let mut lines = Vec::new();
        let mut buf = Vec::new();
        loop {
            buf.clear();
            if reader.read_until(b'\n', &mut buf)? == 0 {
                break;
            }
            let line = String::from_utf8(buf.clone())
                .map_err(|_| anyhow!("文件包含无法用 UTF-8 解析的字节"))?;
            lines.push(line);
        }
        return Ok(lines);
    }
    let mut lines = Vec::new();
    let mut reader = reader;
    let mut buf = Vec::new();
    loop {
        buf.clear();
        if reader.read_until(b'\n', &mut buf)? == 0 {
            break;
        }
        let line = String::from_utf8(buf.clone())
            .map_err(|_| anyhow!("文件包含无法用 UTF-8 解析的字节"))?;
        lines.push(line);
    }
    Ok(lines)
}

fn should_strip_patch_crlf(raw: &str) -> bool {
    // 仅当补丁“结构行”本身使用 CRLF 时，判定为 DOS 风格补丁并执行 strip。
    // 若仅是 hunk 内容行包含 CR（例如目标文件是 CRLF），不应触发该行为。
    let mut has_header_crlf = false;
    let mut has_normal_cmd_crlf = false;
    for line in raw.split_inclusive('\n') {
        if !line.ends_with("\r\n") {
            continue;
        }
        if line.starts_with("--- ")
            || line.starts_with("+++ ")
            || line.starts_with("*** ")
            || line.starts_with("********")
            || line.starts_with("diff --git ")
            || line.starts_with("index ")
            || line.starts_with("Prereq: ")
        {
            has_header_crlf = true;
        }
        let t = line.trim_end_matches('\n').trim_end_matches('\r');
        if looks_like_ed_or_normal(t) {
            has_normal_cmd_crlf = true;
        }
    }
    has_header_crlf || has_normal_cmd_crlf
}

fn synthesize_missing_headers_for_origfile(raw: &str, orig: &Path) -> String {
    let mut first_nonempty = None;
    for l in raw.lines() {
        if !l.trim().is_empty() {
            first_nonempty = Some(l);
            break;
        }
    }
    let Some(first) = first_nonempty else {
        return raw.to_string();
    };

    let p = orig.display().to_string();
    if first.starts_with("@@ ") {
        return format!("--- {}\n+++ {}\n{}", p, p, raw);
    }
    if first.starts_with("***************") {
        return format!("*** {}\n--- {}\n{}", p, p, raw);
    }

    let has_file_headers = raw.contains("\n--- ") || raw.starts_with("--- ") || raw.contains("\n*** ") || raw.starts_with("*** ");
    if has_file_headers {
        return raw.to_string();
    }

    raw.to_string()
}

#[cfg(unix)]
fn open_file_no_follow(path: &Path) -> Result<fs::File> {
    use std::os::unix::fs::OpenOptionsExt;
    Ok(fs::OpenOptions::new()
        .read(true)
        .custom_flags(libc::O_NOFOLLOW)
        .open(path)?)
}

#[cfg(not(unix))]
fn open_file_no_follow(path: &Path) -> Result<fs::File> {
    fs::File::open(path)
}

fn write_lines(path: &Path, lines: &[String], _binary: bool) -> Result<()> {
    #[cfg(unix)]
    {
        use std::os::unix::fs::MetadataExt;
        if let Ok(meta) = fs::metadata(path) {
            if meta.nlink() > 1 {
                let parent = path.parent().unwrap_or(Path::new("."));
                let file_name = path.file_name().and_then(|s| s.to_str()).unwrap_or("patch");
                let mut tmp = parent.join(format!(".{}.patch-tmp-{}", file_name, std::process::id()));
                let mut idx = 0u32;
                while tmp.exists() {
                    idx += 1;
                    tmp = parent.join(format!(".{}.patch-tmp-{}-{}", file_name, std::process::id(), idx));
                }
                {
                    let mut f = fs::File::create(&tmp)?;
                    for l in lines {
                        f.write_all(l.as_bytes())?;
                    }
                }
                fs::rename(&tmp, path)?;
                return Ok(());
            }
        }
    }
    let mut f = fs::File::create(path)?;
    for l in lines {
        f.write_all(l.as_bytes())?;
    }
    Ok(())
}

fn parse_unified_patches<R: BufRead>(reader: &mut R) -> Result<(Vec<FilePatch>, Option<String>)> {
    let mut patches = Vec::<FilePatch>::new();
    let mut current: Option<FilePatch> = None;
    let mut current_hunk: Option<Hunk> = None;
    let mut in_hunks = false;
    let mut prereq: Option<String> = None;
    let mut pending_rename_from: Option<String> = None;
    let mut pending_rename_to: Option<String> = None;
    let mut pending_copy_from: Option<String> = None;
    let mut pending_copy_to: Option<String> = None;
    let mut pending_index: Option<String> = None;
    let mut pending_git_old_path: Option<String> = None;
    let mut pending_git_new_path: Option<String> = None;
    let mut pending_old_mode: Option<String> = None;
    let mut pending_new_mode: Option<String> = None;

    let mut buf = String::new();
    let mut line_no: usize = 0;
    while reader.read_line(&mut buf)? != 0 {
        line_no += 1;
        let line = buf.clone();
        buf.clear();
        let trim = line.trim_end();

        if in_hunks && is_unified_file_boundary(trim) {
            if let Some(h) = current_hunk.take() {
                if let Some(fp) = current.as_mut() {
                    fp.hunks.push(h);
                }
            }
            if let Some(fp) = current.take() {
                if !fp.hunks.is_empty() || fp.rename_to.is_some() || fp.copy_to.is_some() {
                    patches.push(fp);
                }
            }
            in_hunks = false;
            continue;
        }

        // Git 风格 diff：在尚未进入 ---/+++ 时跳过头部行；收集 Prereq。
        if current.is_none() && !line.starts_with("--- ") && !line.starts_with("+++ ") && !line.starts_with("@@ ") {
            if trim.starts_with("Prereq: ") {
                prereq = Some(trim[8..].trim().to_string());
                continue;
            }
            if let Some(rest) = trim.strip_prefix("diff --git ") {
                // 新块开始前，提交仅含 mode 的上一个块。
                if current.is_none() && (pending_rename_to.is_some() || pending_copy_to.is_some()) {
                    patches.push(FilePatch {
                        old_path: pending_rename_from.clone().or(pending_copy_from.clone()),
                        new_path: pending_rename_to.clone().or(pending_copy_to.clone()),
                        old_label: pending_rename_from.clone().or(pending_copy_from.clone()),
                        new_label: pending_rename_to.clone().or(pending_copy_to.clone()),
                        index_line: pending_index.take(),
                        source_format: Some(RejectFormat::Unified),
                        rename_from: pending_rename_from.take(),
                        rename_to: pending_rename_to.take(),
                        copy_from: pending_copy_from.take(),
                        copy_to: pending_copy_to.take(),
                        old_mode: pending_old_mode.take(),
                        new_mode: pending_new_mode.take(),
                        hunks: Vec::new(),
                        timestamp_header: None,
                        malformed_line: None,
                        malformed_text: None,
                        git_binary_patch: false,
                    });
                }
                if current.is_none()
                    && pending_git_new_path.is_some()
                    && (pending_old_mode.is_some() || pending_new_mode.is_some())
                {
                    patches.push(FilePatch {
                        old_path: pending_git_old_path.clone(),
                        new_path: pending_git_new_path.clone(),
                        old_label: pending_git_old_path.clone(),
                        new_label: pending_git_new_path.clone(),
                        index_line: pending_index.take(),
                        source_format: Some(RejectFormat::Unified),
                        rename_from: pending_rename_from.take(),
                        rename_to: pending_rename_to.take(),
                        copy_from: pending_copy_from.take(),
                        copy_to: pending_copy_to.take(),
                        old_mode: pending_old_mode.take(),
                        new_mode: pending_new_mode.take(),
                        hunks: Vec::new(),
                        timestamp_header: None,
                        malformed_line: None,
                        malformed_text: None,
                        git_binary_patch: false,
                    });
                }
                let mut sp = rest.split_whitespace();
                pending_git_old_path = sp.next().map(|s| s.to_string());
                pending_git_new_path = sp.next().map(|s| s.to_string());
                pending_old_mode = None;
                pending_new_mode = None;
                continue;
            }
            if let Some(m) = trim.strip_prefix("old mode ") {
                pending_old_mode = Some(m.trim().to_string());
                continue;
            }
            if let Some(m) = trim.strip_prefix("new mode ") {
                pending_new_mode = Some(m.trim().to_string());
                continue;
            }
            if let Some(m) = trim.strip_prefix("new file mode ") {
                pending_old_mode = Some("000000".to_string());
                pending_new_mode = Some(m.trim().to_string());
                continue;
            }
            if let Some(m) = trim.strip_prefix("deleted file mode ") {
                pending_old_mode = Some(m.trim().to_string());
                pending_new_mode = Some("000000".to_string());
                continue;
            }
            if let Some(v) = trim.strip_prefix("rename from ") {
                pending_rename_from = Some(v.to_string());
                continue;
            }
            if let Some(v) = trim.strip_prefix("rename to ") {
                pending_rename_to = Some(v.to_string());
                continue;
            }
            if let Some(v) = trim.strip_prefix("copy from ") {
                pending_copy_from = Some(v.to_string());
                continue;
            }
            if let Some(v) = trim.strip_prefix("copy to ") {
                pending_copy_to = Some(v.to_string());
                continue;
            }
            if trim.starts_with("Index: ") {
                pending_index = Some(trim.to_string());
                continue;
            }
            if let Some(rest) = trim.strip_prefix("index ") {
                if let Some((_, mode)) = rest.rsplit_once(' ') {
                    if mode.chars().all(|c| c.is_ascii_digit()) {
                        if pending_old_mode.is_none() {
                            pending_old_mode = Some(mode.to_string());
                        }
                        if pending_new_mode.is_none() {
                            pending_new_mode = Some(mode.to_string());
                        }
                    }
                }
                continue;
            }
            if trim.starts_with("Binary files ") {
                if let Some((oldp, newp)) = parse_binary_files_header(trim) {
                    patches.push(FilePatch {
                        old_path: Some(oldp),
                        new_path: Some(newp),
                        old_label: None,
                        new_label: None,
                        index_line: pending_index.take(),
                        source_format: Some(RejectFormat::Unified),
                        rename_from: pending_rename_from.take(),
                        rename_to: pending_rename_to.take(),
                        copy_from: pending_copy_from.take(),
                        copy_to: pending_copy_to.take(),
                        old_mode: pending_old_mode.take(),
                        new_mode: pending_new_mode.take(),
                        hunks: Vec::new(),
                        timestamp_header: None,
                        malformed_line: None,
                        malformed_text: None,
                        git_binary_patch: false,
                    });
                }
                continue;
            }
            if trim.starts_with("GIT binary patch") {
                let oldp = pending_git_old_path.take();
                let newp = pending_git_new_path.take();
                patches.push(FilePatch {
                    old_path: oldp.clone(),
                    new_path: newp.clone(),
                    old_label: oldp,
                    new_label: newp,
                    index_line: pending_index.take(),
                    source_format: Some(RejectFormat::Unified),
                    rename_from: pending_rename_from.take(),
                    rename_to: pending_rename_to.take(),
                    copy_from: pending_copy_from.take(),
                    copy_to: pending_copy_to.take(),
                    old_mode: pending_old_mode.take(),
                    new_mode: pending_new_mode.take(),
                    hunks: Vec::new(),
                    timestamp_header: None,
                    malformed_line: None,
                    malformed_text: None,
                    git_binary_patch: true,
                });
                continue;
            }
            if trim.starts_with("diff --git ")
                || trim.starts_with("index ")
                || trim.starts_with("new file mode ")
                || trim.starts_with("deleted file mode ")
                || trim.starts_with("similarity index ")
                || trim.starts_with("Binary files ")
                || trim.starts_with("GIT binary patch")
                || trim.is_empty()
            {
                continue;
            }
        }

        if line.starts_with("--- ") {
            // 新的文件块开始：提交上一个。
            if let Some(h) = current_hunk.take() {
                if let Some(fp) = current.as_mut() {
                    fp.hunks.push(h);
                }
            }
            if let Some(fp) = current.take() {
                if !fp.hunks.is_empty() || fp.rename_to.is_some() || fp.copy_to.is_some() {
                    patches.push(fp);
                }
            }
            in_hunks = false;
            current = Some(FilePatch {
                old_path: Some(extract_path_from_header(&line)),
                new_path: None,
                old_label: Some(extract_header_label(&line)),
                new_label: None,
                index_line: pending_index.take(),
                source_format: Some(RejectFormat::Unified),
                rename_from: pending_rename_from.take(),
                rename_to: pending_rename_to.take(),
                copy_from: pending_copy_from.take(),
                copy_to: pending_copy_to.take(),
                old_mode: pending_old_mode.take(),
                new_mode: pending_new_mode.take(),
                hunks: Vec::new(),
                timestamp_header: None,
                malformed_line: None,
                malformed_text: None,
                git_binary_patch: false,
            });
            continue;
        }
        if line.starts_with("+++ ") {
            if let Some(fp) = current.as_mut() {
                fp.new_path = Some(extract_path_from_header(&line));
                fp.new_label = Some(extract_header_label(&line));
                fp.timestamp_header = extract_timestamp_from_header(&line);
            }
            continue;
        }

        if line.starts_with("@@ ") {
            if current.is_none() {
                if pending_git_old_path.is_some() || pending_git_new_path.is_some() {
                    let oldp = pending_git_old_path
                        .clone()
                        .or_else(|| pending_git_new_path.clone())
                        .unwrap_or_else(|| "a/unknown".to_string());
                    let newp = pending_git_new_path
                        .clone()
                        .or_else(|| pending_git_old_path.clone())
                        .unwrap_or_else(|| "b/unknown".to_string());
                    current = Some(FilePatch {
                        old_path: Some(oldp.clone()),
                        new_path: Some(newp.clone()),
                        old_label: Some(oldp),
                        new_label: Some(newp),
                        index_line: pending_index.take(),
                        source_format: Some(RejectFormat::Unified),
                        rename_from: pending_rename_from.take(),
                        rename_to: pending_rename_to.take(),
                        copy_from: pending_copy_from.take(),
                        copy_to: pending_copy_to.take(),
                        old_mode: pending_old_mode.take(),
                        new_mode: pending_new_mode.take(),
                        hunks: Vec::new(),
                        timestamp_header: None,
                        malformed_line: None,
                        malformed_text: None,
                        git_binary_patch: false,
                    });
                } else {
                    // 遇到 hunk 但还没有 ---/+++，视为错误。
                    bail!("在缺少文件头（---/+++）的情况下遇到 hunk 头: {}", line.trim());
                }
            }
            if let Some(h) = current_hunk.take() {
                if let Some(fp) = current.as_mut() {
                    fp.hunks.push(h);
                }
            }
            in_hunks = true;
            let (old_start, old_len, new_start, new_len, header_suffix) =
                match parse_hunk_header(&line).context("解析 unified hunk 头失败") {
                    Ok(v) => v,
                    Err(_) => {
                        if let Some(mut fp) = current.take() {
                            fp.malformed_line = Some(line_no);
                            fp.malformed_text = Some(line.trim_end().to_string());
                            patches.push(fp);
                        }
                        return Ok((patches, prereq));
                    }
                };
            current_hunk = Some(Hunk {
                old_start,
                old_len,
                new_start,
                new_len,
                header_suffix,
                lines: Vec::new(),
            });
            continue;
        }

        if in_hunks {
            if line.is_empty() {
                continue;
            }
            if line == "\n" || line == "\r\n" {
                // hunk 间的空行分隔，不计入 hunk 内容。
                continue;
            }
            let first = line.chars().next().unwrap();
            let rest = &line[1..];
            let kind = match first {
                ' ' => HunkLineKind::Context,
                '+' => HunkLineKind::Add,
                '-' => HunkLineKind::Remove,
                '\\' => {
                    // 处理 "\ No newline at end of file" 行：
                    // 表示前一条 hunk 行在原文件/新文件末尾没有换行符。
                    // 这里需要去掉前一条记录的行尾换行，否则 EOF hunk 可能匹配失败。
                    if let Some(h) = current_hunk.as_mut() {
                        if let Some(last) = h.lines.last_mut() {
                            if last.text.ends_with("\r\n") {
                                let new_len = last.text.len().saturating_sub(2);
                                last.text.truncate(new_len);
                            } else if last.text.ends_with('\n') {
                                last.text.pop();
                            }
                        }
                    }
                    continue;
                }
                _ => {
                    if let Some(mut fp) = current.take() {
                        fp.malformed_line = Some(line_no);
                        fp.malformed_text = Some(line.trim_end().to_string());
                        patches.push(fp);
                    }
                    return Ok((patches, prereq));
                }
            };
            if let Some(h) = current_hunk.as_mut() {
                h.lines.push(HunkLine {
                    kind,
                    text: rest.to_string(),
                });
            }
        }
    }

    if let Some(h) = current_hunk.take() {
        if let Some(fp) = current.as_mut() {
            fp.hunks.push(h);
        }
    }
    if let Some(fp) = current.take() {
        if !fp.hunks.is_empty() || fp.rename_to.is_some() || fp.copy_to.is_some() {
            patches.push(fp);
        }
    }

    if pending_rename_to.is_some() || pending_copy_to.is_some() {
        patches.push(FilePatch {
            old_path: pending_rename_from.clone().or(pending_copy_from.clone()),
            new_path: pending_rename_to.clone().or(pending_copy_to.clone()),
            old_label: pending_rename_from.clone().or(pending_copy_from.clone()),
            new_label: pending_rename_to.clone().or(pending_copy_to.clone()),
            index_line: pending_index.take(),
            source_format: Some(RejectFormat::Unified),
            rename_from: pending_rename_from,
            rename_to: pending_rename_to,
            copy_from: pending_copy_from,
            copy_to: pending_copy_to,
            old_mode: pending_old_mode.clone(),
            new_mode: pending_new_mode.clone(),
            hunks: Vec::new(),
            timestamp_header: None,
            malformed_line: None,
            malformed_text: None,
            git_binary_patch: false,
        });
    }

    if pending_git_new_path.is_some() && (pending_old_mode.is_some() || pending_new_mode.is_some()) {
        patches.push(FilePatch {
            old_path: pending_git_old_path,
            new_path: pending_git_new_path,
            old_label: None,
            new_label: None,
            index_line: pending_index,
            source_format: Some(RejectFormat::Unified),
            rename_from: None,
            rename_to: None,
            copy_from: None,
            copy_to: None,
            old_mode: pending_old_mode,
            new_mode: pending_new_mode,
            hunks: Vec::new(),
            timestamp_header: None,
            malformed_line: None,
            malformed_text: None,
            git_binary_patch: false,
        });
    }

    Ok((patches, prereq))
}

fn parse_binary_files_header(line: &str) -> Option<(String, String)> {
    let rest = line.strip_prefix("Binary files ")?;
    let (a, b) = rest.split_once(" and ")?;
    let newp = b.strip_suffix(" differ")?;
    Some((a.trim().to_string(), newp.trim().to_string()))
}

fn is_unified_file_boundary(line: &str) -> bool {
    line.starts_with("diff ")
        || line.starts_with("index ")
        || line.starts_with("new file mode ")
        || line.starts_with("deleted file mode ")
        || line.starts_with("old mode ")
        || line.starts_with("new mode ")
        || line.starts_with("rename from ")
        || line.starts_with("rename to ")
        || line.starts_with("copy from ")
        || line.starts_with("copy to ")
        || line.starts_with("similarity index ")
        || line.starts_with("dissimilarity index ")
        || line.starts_with("Binary files ")
        || line.starts_with("GIT binary patch")
}

/// 从 --- / +++ 行提取路径。git 风格为 "--- a/path" / "+++ b/path" 或 "/dev/null"。
fn extract_path_from_header(line: &str) -> String {
    let mut s = line[4..].trim_end().to_string();
    if let Some(idx) = s.find('\t') {
        s.truncate(idx);
    }
    unquote_patch_path(&s)
}

fn unquote_patch_path(raw: &str) -> String {
    if raw.len() >= 2 && raw.starts_with('"') && raw.ends_with('"') {
        decode_c_quoted(&raw[1..raw.len() - 1])
    } else {
        raw.to_string()
    }
}

fn decode_c_quoted(s: &str) -> String {
    let mut out = String::new();
    let mut it = s.chars().peekable();
    while let Some(c) = it.next() {
        if c != '\\' {
            out.push(c);
            continue;
        }
        match it.next() {
            Some('n') => out.push('\n'),
            Some('r') => out.push('\r'),
            Some('t') => out.push('\t'),
            Some('"') => out.push('"'),
            Some('\\') => out.push('\\'),
            Some(d @ '0'..='7') => {
                let mut v = d.to_digit(8).unwrap_or(0);
                for _ in 0..2 {
                    if let Some(p) = it.peek().copied() {
                        if ('0'..='7').contains(&p) {
                            let _ = it.next();
                            v = (v * 8) + p.to_digit(8).unwrap_or(0);
                        } else {
                            break;
                        }
                    }
                }
                out.push(char::from_u32(v).unwrap_or('?'));
            }
            Some(other) => out.push(other),
            None => {}
        }
    }
    out
}

fn extract_header_label(line: &str) -> String {
    line.get(4..).unwrap_or("").trim_end().to_string()
}

/// 从 --- / +++ 行提取 \t 后的时间戳字符串（用于 -T/-Z）。
fn extract_timestamp_from_header(line: &str) -> Option<String> {
    let rest = line.get(4..)?.trim_end();
    let after_tab = rest.split('\t').nth(1)?;
    let s = after_tab.trim();
    if s.is_empty() {
        None
    } else {
        Some(s.to_string())
    }
}

fn parse_hunk_header(line: &str) -> Result<(usize, usize, usize, usize, Option<String>)> {
    // 形如：@@ -l,s +l,s @@ ... 或无 ",s"
    // 我们只解析数字部分，忽略后面的函数名等。
    let end = line.find("@@").and_then(|i| line[i + 2..].find("@@").map(|j| i + 2 + j));
    let core = if let Some(end) = end {
        &line[3..end].trim()
    } else {
        line.trim_matches('@').trim()
    };
    let header_suffix = if let Some(end) = end {
        let tail = line[end + 2..].trim_end_matches('\n').trim_end_matches('\r');
        if tail.trim().is_empty() {
            None
        } else {
            Some(tail.to_string())
        }
    } else {
        None
    };

    let mut parts = core.split_whitespace();
    let old_part = parts
        .next()
        .ok_or_else(|| anyhow!("缺少旧文件范围"))?;
    let new_part = parts
        .next()
        .ok_or_else(|| anyhow!("缺少新文件范围"))?;

    let (old_start, old_len) = parse_range(old_part.trim_start_matches('-'))?;
    let (new_start, new_len) = parse_range(new_part.trim_start_matches('+'))?;

    Ok((old_start, old_len, new_start, new_len, header_suffix))
}

fn parse_range(s: &str) -> Result<(usize, usize)> {
    if let Some((a, b)) = s.split_once(',') {
        let start: usize = a.parse()?;
        let len: usize = b.parse()?;
        Ok((start, len))
    } else {
        let start: usize = s.parse()?;
        Ok((start, 1))
    }
}

/// 反向补丁：交换 hunk 的 old/new 及 Add/Remove，对应 C 版 pch_swap。
fn swap_hunk_for_reverse(hunk: &Hunk) -> Hunk {
    let lines = hunk
        .lines
        .iter()
        .map(|hl| {
            let kind = match hl.kind {
                HunkLineKind::Context => HunkLineKind::Context,
                HunkLineKind::Add => HunkLineKind::Remove,
                HunkLineKind::Remove => HunkLineKind::Add,
            };
            HunkLine {
                kind,
                text: hl.text.clone(),
            }
        })
        .collect();
    Hunk {
        old_start: hunk.new_start,
        old_len: hunk.new_len,
        new_start: hunk.old_start,
        new_len: hunk.old_len,
        header_suffix: hunk.header_suffix.clone(),
        lines,
    }
}

fn apply_hunks_with_fuzz(
    target_lines: &mut Vec<String>,
    hunks: &[Hunk],
    fuzz: i32,
    ifdef_name: Option<&str>,
    ignore_whitespace: bool,
    binary: bool,
    quiet: bool,
) -> Result<()> {
    // 近似 C 版 locate_hunk 行为：
    // - 使用 old_start + offset 作为首选位置
    // - 在 ±fuzz 行范围内寻找一个能完整匹配上下文/删除行的起点
    // - 找不到就认为本文件的补丁失败
    let mut offset: isize = 0;

    for (idx, h) in hunks.iter().enumerate() {
        let base = if h.old_start == 0 { 0 } else { h.old_start as isize - 1 };
        let guess = base + offset;
        if guess < 0 {
            bail!("hunk {} 起始位置非法", idx + 1);
        }

        let start = locate_hunk_position(target_lines, h, guess as usize, fuzz, ignore_whitespace, binary)
            .with_context(|| format!("无法定位 hunk #{} 到目标文件", idx + 1))?;
        if !quiet && start as isize != guess {
            let off = start as isize - guess;
            let _ = writeln!(
                io::stderr(),
                "Hunk #{} succeeded at {} (offset {} lines).",
                idx + 1,
                start + 1,
                off
            );
        }

        let delta = if let Some(name) = ifdef_name {
            apply_single_hunk_ifdef(target_lines, h, start, name)
                .with_context(|| format!("应用 hunk #{} 失败（ifdef）", idx + 1))?;
            // ifdef 块增加 3 行（#ifndef / #else / #endif），替换后总行数 3 + old_len + new_len
            3 + h.new_len as isize - h.old_len as isize
        } else {
            apply_single_hunk(target_lines, h, start, binary)
                .with_context(|| format!("应用 hunk #{} 失败", idx + 1))?;
            h.new_len as isize - h.old_len as isize
        };
        offset += delta;
    }

    Ok(())
}

fn apply_hunks_with_report(
    target_lines: &mut Vec<String>,
    hunks: &[Hunk],
    fuzz: i32,
    ifdef_name: Option<&str>,
    ignore_whitespace: bool,
    binary: bool,
) -> Vec<HunkApplyReport> {
    let mut offset: isize = 0;
    let mut out = Vec::new();

    for (idx, h) in hunks.iter().enumerate() {
        let base = if h.old_start == 0 { 0 } else { h.old_start as isize - 1 };
        let guess = base + offset;
        if guess < 0 {
            out.push(HunkApplyReport {
                index: idx + 1,
                hunk: h.clone(),
                success: false,
                at_line: 1,
                offset: 0,
                fuzz_used: 0,
            });
            continue;
        }

        let before_len = target_lines.len();
        let located = locate_hunk_position(target_lines, h, guess as usize, fuzz, ignore_whitespace, binary);
        match located {
            Ok(start) => {
                let off = start as isize - guess;
                let mut fuzz_used = fuzz.max(0);
                for k in 0..=fuzz.max(0) {
                    if match_hunk_at(target_lines, h, start, k, ignore_whitespace, binary).is_ok() {
                        fuzz_used = k;
                        break;
                    }
                }
                let heuristic = estimate_fuzz_used(h, start, guess as usize, before_len, off);
                if heuristic > fuzz_used {
                    fuzz_used = heuristic;
                }
                let applied = if let Some(name) = ifdef_name {
                    apply_single_hunk_ifdef(target_lines, h, start, name).map(|_| {
                        3 + h.new_len as isize - h.old_len as isize
                    })
                } else {
                    apply_single_hunk(target_lines, h, start, binary)
                        .map(|_| h.new_len as isize - h.old_len as isize)
                };
                match applied {
                    Ok(delta) => {
                        offset += delta;
                        out.push(HunkApplyReport {
                            index: idx + 1,
                            hunk: h.clone(),
                            success: true,
                            at_line: start + 1,
                            offset: off,
                            fuzz_used,
                        });
                    }
                    Err(_) => {
                        out.push(HunkApplyReport {
                            index: idx + 1,
                            hunk: h.clone(),
                            success: false,
                            at_line: (guess as usize) + 1,
                            offset: 0,
                            fuzz_used: 0,
                        });
                    }
                }
            }
            Err(_) => {
                out.push(HunkApplyReport {
                    index: idx + 1,
                    hunk: h.clone(),
                    success: false,
                    at_line: (guess as usize) + 1,
                    offset: 0,
                    fuzz_used: 0,
                });
            }
        }
    }

    out
}

fn estimate_fuzz_used(hunk: &Hunk, start: usize, guess: usize, target_len: usize, offset: isize) -> i32 {
    let expected_old = hunk
        .lines
        .iter()
        .filter(|hl| matches!(hl.kind, HunkLineKind::Context | HunkLineKind::Remove))
        .count();
    let available_old = target_len.saturating_sub(start);
    let mut fuzz = 0i32;
    if expected_old > available_old {
        fuzz = (expected_old - available_old) as i32;
    }

    let mut prefix_ctx = 0usize;
    for hl in &hunk.lines {
        if hl.kind == HunkLineKind::Context {
            prefix_ctx += 1;
        } else {
            break;
        }
    }
    let mut suffix_ctx = 0usize;
    for hl in hunk.lines.iter().rev() {
        if hl.kind == HunkLineKind::Context {
            suffix_ctx += 1;
        } else {
            break;
        }
    }

    if hunk.old_start == 1 && start > guess && prefix_ctx < suffix_ctx {
        fuzz = fuzz.max(1);
    }

    if hunk.old_start > 1 && prefix_ctx > suffix_ctx && suffix_ctx > 0 && offset == 0 {
        let hunk_end = start.saturating_add(hunk.old_len);
        if target_len > 0 && hunk_end >= target_len.saturating_sub(1) {
            fuzz = fuzz.max(1);
        }
    }

    fuzz
}

fn locate_hunk_position(
    target_lines: &[String],
    hunk: &Hunk,
    guess: usize,
    fuzz: i32,
    ignore_whitespace: bool,
    binary: bool,
) -> Result<usize> {
    if target_lines.is_empty() {
        if hunk.old_len == 0 {
            return Ok(0);
        }
        bail!("目标文件为空，无法应用非空 hunk");
    }

    let len = target_lines.len();

    // 首先尝试精确位置。
    if match_hunk_at(target_lines, hunk, guess, fuzz, ignore_whitespace, binary).is_ok() {
        return Ok(guess);
    }

    // 从 guess 向两侧扩展到全文件范围搜索位置；fuzz 仍用于匹配容忍度。
    let max_span = guess.max(len.saturating_sub(1));
    for offset in 1..=max_span {
        let down = guess.saturating_add(offset);
        if down < len && match_hunk_at(target_lines, hunk, down, fuzz, ignore_whitespace, binary).is_ok() {
            return Ok(down);
        }
        if let Some(up) = guess.checked_sub(offset) {
            if match_hunk_at(target_lines, hunk, up, fuzz, ignore_whitespace, binary).is_ok() {
                return Ok(up);
            }
        }
    }

    bail!("在 fuzz={} 范围内未找到可用位置", fuzz);
}

/// -l：将任意连续空白（空格/制表）视为等价。
fn normalize_blanks(s: &str) -> String {
    let mut out = String::new();
    let mut was_blank = false;
    for c in s.chars() {
        let blank = c == ' ' || c == '\t';
        if blank {
            if !was_blank {
                out.push(' ');
            }
            was_blank = true;
        } else {
            out.push(c);
            was_blank = false;
        }
    }
    out.trim_end().to_string()
}

fn match_hunk_at(
    target_lines: &[String],
    hunk: &Hunk,
    start: usize,
    fuzz: i32,
    ignore_whitespace: bool,
    binary: bool,
) -> Result<()> {
    let mut idx = start;
    let fuzz_allow = fuzz.max(0) as usize;
    for (i, hl) in hunk.lines.iter().enumerate() {
        match hl.kind {
            HunkLineKind::Context | HunkLineKind::Remove => {
                if idx >= target_lines.len() {
                    // 允许在 fuzz 范围内忽略“文件尾缺失的上下文行”。
                    if hl.kind == HunkLineKind::Context {
                        let tail_ctx = hunk.lines[i..]
                            .iter()
                            .filter(|x| x.kind == HunkLineKind::Context)
                            .count();
                        if tail_ctx <= fuzz_allow {
                            return Ok(());
                        }
                    }
                    bail!("hunk 超出原文件长度");
                }
                let line = &target_lines[idx];
                let eq = if ignore_whitespace {
                    normalize_blanks(&normalize_line_endings(&hl.text, binary))
                        == normalize_blanks(&normalize_line_endings(line, binary))
                } else {
                    normalize_line_endings(&hl.text, binary) == normalize_line_endings(line, binary)
                };
                if !eq {
                    bail!("hunk 上下文/删除行与原文件不匹配");
                }
                idx += 1;
            }
            HunkLineKind::Add => {
                // 新增行不参与匹配。
            }
        }
    }
    Ok(())
}

fn apply_single_hunk(
    target_lines: &mut Vec<String>,
    hunk: &Hunk,
    start: usize,
    binary: bool,
) -> Result<()> {
    // 重新从 start 开始，根据 hunk 行重建这一段；上下文行保留目标文件原始换行风格。
    let mut new_segment = Vec::<String>::new();
    let mut cur = start;
    let use_crlf = target_lines.iter().any(|l| l.ends_with("\r\n"));
    for hl in &hunk.lines {
        match hl.kind {
            HunkLineKind::Context | HunkLineKind::Remove => {
                if hl.kind == HunkLineKind::Context {
                    if cur < target_lines.len() {
                        new_segment.push(target_lines[cur].clone());
                    }
                }
                cur = cur.saturating_add(1);
            }
            HunkLineKind::Add => {
                new_segment.push(adapt_line_ending(&hl.text, use_crlf, binary));
            }
        }
    }

    let expected_remove = hunk
        .lines
        .iter()
        .filter(|hl| matches!(hl.kind, HunkLineKind::Context | HunkLineKind::Remove))
        .count();
    let remove_len = expected_remove.min(target_lines.len().saturating_sub(start));
    if start + remove_len > target_lines.len() {
        bail!("hunk 删除范围超出原文件长度");
    }
    target_lines.splice(start..start + remove_len, new_segment.into_iter());
    Ok(())
}

fn normalize_line_endings(s: &str, binary: bool) -> String {
    if binary {
        s.to_string()
    } else if s.ends_with("\r\n") {
        let mut t = s.to_string();
        t.truncate(t.len() - 2);
        t.push('\n');
        t
    } else {
        s.to_string()
    }
}

fn adapt_line_ending(s: &str, use_crlf: bool, binary: bool) -> String {
    if binary || !use_crlf {
        return s.to_string();
    }
    if s.ends_with("\n") && !s.ends_with("\r\n") {
        let mut t = s.to_string();
        t.truncate(t.len() - 1);
        t.push_str("\r\n");
        t
    } else {
        s.to_string()
    }
}

/// 应用单个 hunk 为 ifdef 块：用 #ifndef NAME + 旧内容 + #else + 新内容 + #endif 替换该段。
fn apply_single_hunk_ifdef(
    target_lines: &mut Vec<String>,
    hunk: &Hunk,
    start: usize,
    name: &str,
) -> Result<()> {
    let remove_len = hunk.old_len;
    if start + remove_len > target_lines.len() {
        bail!("hunk 删除范围超出原文件长度");
    }
    let old_block: Vec<String> = target_lines[start..start + remove_len].to_vec();
    let mut new_segment = Vec::<String>::new();
    for hl in &hunk.lines {
        match hl.kind {
            HunkLineKind::Context | HunkLineKind::Remove => {
                if hl.kind == HunkLineKind::Context {
                    new_segment.push(hl.text.clone());
                }
            }
            HunkLineKind::Add => {
                new_segment.push(hl.text.clone());
            }
        }
    }
    let mut replacement = Vec::new();
    replacement.push(format!("#ifndef {}\n", name));
    replacement.extend(old_block);
    replacement.push("#else\n".to_string());
    replacement.extend(new_segment);
    replacement.push("#endif\n".to_string());
    target_lines.splice(start..start + remove_len, replacement.into_iter());
    Ok(())
}

/// 将补丁中的路径转为本地路径。GNU：未指定 -p 时去掉所有前导目录（仅留 basename）；
/// 相邻斜线视为一个分量。
fn map_patch_path_to_fs(raw: &str, strip: Option<i32>) -> Result<PathBuf> {
    let comps: Vec<&str> = raw.split('/').filter(|s| !s.is_empty()).collect();
    let is_abs = raw.starts_with('/');
    let strip_n = effective_strip_count(raw, strip);
    if comps.is_empty() {
        bail!("空文件名");
    }
    if comps.len() <= strip_n {
        // 与 GNU 行为对齐：过度 strip 不视为致命解析错误，返回原路径，后续按“找不到文件”处理。
        return Ok(PathBuf::from(raw));
    }
    let tail = &comps[strip_n..];
    let mut out = if is_abs { PathBuf::from("/") } else { PathBuf::new() };
    for c in tail {
        out.push(c);
    }
    Ok(out)
}

fn is_potentially_dangerous_path(path: &Path) -> bool {
    if path.is_absolute() {
        return !is_allowed_absolute_patch_path(path);
    }
    path.components().any(|c| matches!(c, std::path::Component::ParentDir))
}

fn is_allowed_absolute_patch_path(path: &Path) -> bool {
    let mut allowed: Vec<PathBuf> = vec![PathBuf::from("/tmp")];
    for k in ["TMPDIR", "TMP", "TEMP"] {
        if let Ok(v) = std::env::var(k) {
            if !v.is_empty() {
                allowed.push(PathBuf::from(v));
            }
        }
    }
    allowed.into_iter().any(|base| path.starts_with(base))
}

fn is_overstripped_patch_path(raw: &str, strip: Option<i32>) -> bool {
    let strip_n = effective_strip_count(raw, strip);
    if strip_n == 0 {
        return false;
    }
    let comps = raw.split('/').filter(|s| !s.is_empty()).count();
    comps <= strip_n
}

fn effective_strip_count(raw: &str, strip: Option<i32>) -> usize {
    match strip {
        Some(n) => n.max(0) as usize,
        None => {
            // GNU 兼容：未显式指定 -p 时，对 git 风格 a/ b/ 路径默认去掉一层前缀。
            if raw != "/dev/null" {
                if let Some(rest) = raw.strip_prefix("a/") {
                    if !rest.is_empty() {
                        return 1;
                    }
                }
                if let Some(rest) = raw.strip_prefix("b/") {
                    if !rest.is_empty() {
                        return 1;
                    }
                }
            }
            0
        }
    }
}

fn remove_empty_parent_dirs(path: &Path) -> Result<()> {
    let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
    let mut cur = match path.parent() {
        Some(p) => p.to_path_buf(),
        None => return Ok(()),
    };
    loop {
        if cur.as_os_str().is_empty() || cur == Path::new("/") || cur == cwd {
            break;
        }
        match fs::remove_dir(&cur) {
            Ok(_) => {
                if let Some(parent) = cur.parent() {
                    cur = parent.to_path_buf();
                } else {
                    break;
                }
            }
            Err(_) => break,
        }
    }
    Ok(())
}

/// Generate reject file path consistent with GNU patch behavior.
/// For a file like "1.txt", returns "1.txt.rej".
/// For a file like "README" (no extension), returns "README.rej".
fn reject_path_for(target_path: &Path) -> PathBuf {
    let mut p = target_path.to_path_buf();
    let orig_ext = p.extension()
        .map(|e| e.to_string_lossy().to_string())
        .unwrap_or_default();
    let new_ext = if orig_ext.is_empty() {
        "rej".to_string()
    } else {
        format!("{}.rej", orig_ext)
    };
    p.set_extension(new_ext);
    p
}

fn force_make_writable(path: &Path) -> Result<()> {
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        let meta = fs::metadata(path)?;
        let mut mode = meta.permissions().mode();
        mode |= 0o200;
        fs::set_permissions(path, fs::Permissions::from_mode(mode))?;
        Ok(())
    }
    #[cfg(not(unix))]
    {
        let mut perm = fs::metadata(path)?.permissions();
        perm.set_readonly(false);
        fs::set_permissions(path, perm)?;
        Ok(())
    }
}

fn symlink_target_from_hunks(hunks: &[Hunk]) -> Option<String> {
    let mut out = String::new();
    for h in hunks {
        for l in &h.lines {
            if l.kind == HunkLineKind::Add {
                out.push_str(&l.text);
            }
        }
    }
    if out.is_empty() {
        None
    } else {
        Some(out.trim_end_matches('\n').trim_end_matches('\r').to_string())
    }
}

fn is_invalid_target_name(path: &Path) -> bool {
    if is_potentially_dangerous_path(path) {
        return true;
    }
    let cwd = match std::env::current_dir() {
        Ok(c) => c,
        Err(_) => return false,
    };
    let parent = match path.parent() {
        Some(p) if !p.as_os_str().is_empty() => p,
        _ => return false,
    };
    let mut cur = PathBuf::new();
    for c in parent.components() {
        cur.push(c.as_os_str());
        if let Ok(meta) = fs::symlink_metadata(&cur) {
            if meta.file_type().is_symlink() {
                return match fs::canonicalize(parent) {
                    Ok(real_parent) => !real_parent.starts_with(&cwd),
                    Err(_) => true,
                };
            }
        }
    }
    false
}

fn apply_git_mode(path: &Path, mode_str: &str) -> Result<()> {
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        let parsed = u32::from_str_radix(mode_str, 8).unwrap_or(0o100644);
        let perm = parsed & 0o7777;
        fs::set_permissions(path, fs::Permissions::from_mode(perm))?;
    }
    Ok(())
}

#[cfg(unix)]
fn create_symlink_at(target: &str, link: &Path) -> Result<()> {
    use std::os::unix::fs::symlink;
    if let Some(parent) = link.parent() {
        let _ = fs::create_dir_all(parent);
    }
    symlink(target, link)?;
    Ok(())
}

#[cfg(not(unix))]
fn create_symlink_at(_target: &str, _link: &Path) -> Result<()> {
    bail!("当前平台不支持符号链接创建")
}

fn patch_requires_existing_file(hunks: &[Hunk]) -> bool {
    hunks.iter().any(|h| h.old_len > 0)
}

/// 在不修改原文件的前提下，模拟逐个应用 hunk，收集第一个真正失败的 hunk。
/// 模拟行为：一旦某个 hunk 无法定位或应用，就停止后续 hunk，并只返回该 hunk。
fn collect_failed_hunks_for_reject(
    orig_lines: &[String],
    hunks: &[Hunk],
    fuzz: i32,
    ignore_whitespace: bool,
    binary: bool,
) -> Vec<Hunk> {
    let mut lines = orig_lines.to_vec();
    let mut offset: isize = 0;
    let mut failed = Vec::new();

    for h in hunks {
        let guess = h.old_start as isize - 1 + offset;
        if guess < 0 {
            failed.push(h.clone());
            break;
        }
        let res = locate_hunk_position(&lines, h, guess as usize, fuzz, ignore_whitespace, binary)
            .and_then(|pos| apply_single_hunk(&mut lines, h, pos, binary).map(|_| ()));
        match res {
            Ok(()) => {
                offset += h.new_len as isize - h.old_len as isize;
            }
            Err(_) => {
                failed.push(h.clone());
                break;
            }
        }
    }

    failed
}

fn write_reject_hunks(
    config: &PatchConfig,
    rej_path: &Path,
    append: bool,
    target: &Path,
    index_line: Option<&str>,
    source_format: Option<RejectFormat>,
    old_path: Option<&str>,
    new_path: Option<&str>,
    old_label: Option<&str>,
    new_label: Option<&str>,
    hunks: &[Hunk],
) -> Result<()> {
    use std::fs::OpenOptions;

    if hunks.is_empty() {
        return Ok(());
    }

    if !append && (rej_path.exists() || fs::symlink_metadata(rej_path).is_ok()) {
        let _ = fs::remove_file(rej_path);
    }

    let mut f = OpenOptions::new()
        .create(true)
        .write(true)
        .append(append)
        .truncate(!append)
        .open(&rej_path)
        .with_context(|| format!("无法打开 reject 文件 {}", rej_path.display()))?;

    let fmt = config
        .reject_format
        .or(source_format)
        .unwrap_or(RejectFormat::Unified);
    match fmt {
        RejectFormat::Unified => {
            if let Some(idx) = index_line {
                writeln!(f, "{}", idx)?;
            }
            let old = old_label
                .or(old_path)
                .unwrap_or_else(|| target.to_str().unwrap_or("file"));
            let new = new_label
                .or(new_path)
                .unwrap_or_else(|| target.to_str().unwrap_or("file"));
            writeln!(f, "--- {}", old)?;
            writeln!(f, "+++ {}", new)?;
            for h in hunks {
                write_unified_hunk(&mut f, h)?;
            }
        }
        RejectFormat::Context => {
            if let Some(idx) = index_line {
                writeln!(f, "{}", idx)?;
            }
            let old = old_label
                .or(old_path)
                .unwrap_or_else(|| target.to_str().unwrap_or("file"));
            let new = new_label
                .or(new_path)
                .unwrap_or_else(|| target.to_str().unwrap_or("file"));
            writeln!(f, "*** {}", old)?;
            writeln!(f, "--- {}", new)?;
            for h in hunks {
                write_context_hunk(&mut f, h, target)?;
            }
        }
    }
    Ok(())
}

fn write_unified_hunk<W: Write>(w: &mut W, h: &Hunk) -> io::Result<()> {
    let old_len = h.old_len.max(0);
    let new_len = h.new_len.max(0);
    let old_part = if old_len == 1 {
        format!("{}", h.old_start)
    } else {
        format!("{},{}", h.old_start, old_len)
    };
    let new_part = if new_len == 1 {
        format!("{}", h.new_start)
    } else {
        format!("{},{}", h.new_start, new_len)
    };
    if let Some(sfx) = &h.header_suffix {
        writeln!(w, "@@ -{} +{} @@{}", old_part, new_part, sfx)?;
    } else {
        writeln!(w, "@@ -{} +{} @@", old_part, new_part)?;
    }
    for line in &h.lines {
        let prefix = match line.kind {
            HunkLineKind::Context => ' ',
            HunkLineKind::Add => '+',
            HunkLineKind::Remove => '-',
        };
        write!(w, "{}", prefix)?;
        // HunkLine.text 已包含换行。
        write!(w, "{}", line.text)?;
    }
    Ok(())
}

fn write_context_hunk<W: Write>(w: &mut W, h: &Hunk, _target: &Path) -> io::Result<()> {
    let old_end = h.old_start.saturating_add(h.old_len.saturating_sub(1));
    let new_end = h.new_start.saturating_add(h.new_len.saturating_sub(1));

    if let Some(sfx) = &h.header_suffix {
        writeln!(w, "***************{}", sfx)?;
    } else {
        writeln!(w, "***************")?;
    }
    if h.old_len == 0 {
        writeln!(w, "*** 0 ****")?;
    } else if h.old_len == 1 {
        writeln!(w, "*** {} ****", h.old_start)?;
    } else {
        writeln!(w, "*** {},{} ****", h.old_start, old_end)?;
    }
    let mut i = 0usize;
    while i < h.lines.len() {
        let line = &h.lines[i];
        match line.kind {
            HunkLineKind::Context => {
                write!(w, "  ")?; // context
                write!(w, "{}", line.text)?;
            }
            HunkLineKind::Remove => {
                if i + 1 < h.lines.len() && matches!(h.lines[i + 1].kind, HunkLineKind::Add) {
                    write!(w, "! ")?;
                } else {
                    write!(w, "- ")?;
                }
                write!(w, "{}", line.text)?;
            }
            _ => {}
        }
        i += 1;
    }
    if h.new_len == 0 {
        writeln!(w, "--- 0 ----")?;
    } else if h.new_len == 1 {
        writeln!(w, "--- {} ----", h.new_start)?;
    } else {
        writeln!(w, "--- {},{} ----", h.new_start, new_end)?;
    }
    let mut i = 0usize;
    while i < h.lines.len() {
        let line = &h.lines[i];
        match line.kind {
            HunkLineKind::Context => {
                write!(w, "  ")?;
                write!(w, "{}", line.text)?;
            }
            HunkLineKind::Add => {
                if i > 0 && matches!(h.lines[i - 1].kind, HunkLineKind::Remove) {
                    write!(w, "! ")?;
                } else {
                    write!(w, "+ ")?;
                }
                write!(w, "{}", line.text)?;
            }
            _ => {}
        }
        i += 1;
    }
    Ok(())
}



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

    fn sample_hunk(lines: Vec<HunkLine>) -> Hunk {
        Hunk {
            old_start: 1,
            old_len: 1,
            new_start: 1,
            new_len: 1,
            header_suffix: None,
            lines,
        }
    }

    fn hl(kind: HunkLineKind, text: &str) -> HunkLine {
        HunkLine {
            kind,
            text: text.to_string(),
        }
    }

    #[test]
    fn test_quote_path_styles() {
        let p = Path::new("a b.txt");
        assert_eq!(quote_path(p, Some("literal")), "a b.txt");
        assert_eq!(quote_path(p, Some("shell")), "'a b.txt'");
        assert_eq!(quote_path(p, Some("shell-always")), "'a b.txt'");
        assert_eq!(quote_path(Path::new("ab"), Some("shell")), "ab");
    }

    #[test]
    fn test_c_quote_and_escape_backslash() {
        assert_eq!(c_quote("a\tb\n"), "\"a\\tb\\n\"");
        assert_eq!(escape_backslash("a\\b"), "a\\\\b");
    }

    #[test]
    fn test_looks_like_ed_or_normal() {
        assert!(looks_like_ed_or_normal("12a13"));
        assert!(looks_like_ed_or_normal("10,12c20,21"));
        assert!(!looks_like_ed_or_normal("@@ -1,1 +1,1 @@"));
        assert!(!looks_like_ed_or_normal("abc"));
    }

    #[test]
    fn test_parse_normal_command() {
        let (os, ol, ns, nl, cmd) = parse_normal_command("1,3c4,6").unwrap();
        assert_eq!((os, ol, ns, nl, cmd), (1, 2, 4, 3, 'c'));

        let (os, ol, ns, nl, cmd) = parse_normal_command("5a8").unwrap();
        assert_eq!((os, ol, ns, nl, cmd), (5, 0, 6, 8, 'a'));
    }

    #[test]
    fn test_parse_context_range() {
        assert_eq!(parse_context_range("12,15").unwrap(), (12, 4));
        assert_eq!(parse_context_range("9").unwrap(), (9, 1));
        assert_eq!(parse_context_range("0").unwrap(), (0, 0));
    }

    #[test]
    fn test_parse_binary_header_and_boundary() {
        let got = parse_binary_files_header("Binary files a/x and b/y differ").unwrap();
        assert_eq!(got.0, "a/x");
        assert_eq!(got.1, "b/y");
        assert!(is_unified_file_boundary("diff --git a/x b/x"));
        assert!(is_unified_file_boundary("rename from old"));
        assert!(!is_unified_file_boundary("@@ -1 +1 @@"));
    }

    #[test]
    fn test_unquote_and_decode_c_quoted() {
        assert_eq!(unquote_patch_path("\"a\\tb\""), "a\tb");
        assert_eq!(decode_c_quoted("a\\040b"), "a b");
        assert_eq!(decode_c_quoted("a\\n"), "a\n");
    }

    #[test]
    fn test_extract_path_and_timestamp() {
        assert_eq!(extract_path_from_header("--- a/file.txt\t2024-01-01 00:00:00 +0000\n"), "a/file.txt");
        assert_eq!(
            extract_timestamp_from_header("+++ b/file.txt\t2024-01-01 00:00:00 +0000\n").as_deref(),
            Some("2024-01-01 00:00:00 +0000")
        );
    }

    #[test]
    fn test_parse_hunk_header_and_range() {
        let (os, ol, ns, nl, sfx) = parse_hunk_header("@@ -10,2 +20,3 @@ fn name\n").unwrap();
        assert_eq!((os, ol, ns, nl), (10, 2, 20, 3));
        assert_eq!(sfx.as_deref(), Some(" fn name"));
        assert_eq!(parse_range("7,5").unwrap(), (7, 5));
        assert_eq!(parse_range("9").unwrap(), (9, 1));
    }

    #[test]
    fn test_swap_hunk_for_reverse() {
        let h = sample_hunk(vec![
            hl(HunkLineKind::Context, "ctx\n"),
            hl(HunkLineKind::Add, "add\n"),
            hl(HunkLineKind::Remove, "rm\n"),
        ]);
        let r = swap_hunk_for_reverse(&h);
        assert_eq!(r.lines[0].kind, HunkLineKind::Context);
        assert_eq!(r.lines[1].kind, HunkLineKind::Remove);
        assert_eq!(r.lines[2].kind, HunkLineKind::Add);
    }

    #[test]
    fn test_normalize_blank_and_line_endings() {
        assert_eq!(normalize_blanks("a\t\t b   c   \n"), "a b c");
        assert_eq!(normalize_line_endings("x\r\n", false), "x\n");
        assert_eq!(normalize_line_endings("x\r\n", true), "x\r\n");
        assert_eq!(adapt_line_ending("x\n", true, false), "x\r\n");
    }

    #[test]
    fn test_path_strip_and_overstrip() {
        assert_eq!(effective_strip_count("a/src/file.rs", None), 1);
        assert_eq!(effective_strip_count("src/file.rs", None), 0);
        assert!(is_overstripped_patch_path("a/file", Some(2)));
        assert!(!is_overstripped_patch_path("a/b/file", Some(2)));
    }

    #[test]
    fn test_map_patch_path_to_fs() {
        let p = map_patch_path_to_fs("a/src/main.rs", None).unwrap();
        assert_eq!(p, PathBuf::from("src/main.rs"));

        let p2 = map_patch_path_to_fs("x/y/z.txt", Some(1)).unwrap();
        assert_eq!(p2, PathBuf::from("y/z.txt"));

        let p3 = map_patch_path_to_fs("a.txt", Some(2)).unwrap();
        assert_eq!(p3, PathBuf::from("a.txt"));
    }

    #[test]
    fn test_path_safety_checks() {
        assert!(is_potentially_dangerous_path(Path::new("../../etc/passwd")));
        assert!(!is_potentially_dangerous_path(Path::new("safe/path")));
        assert!(is_allowed_absolute_patch_path(Path::new("/tmp/ok")));
    }

    #[test]
    fn test_match_hunk_at_with_and_without_whitespace_mode() {
        let target = vec!["a  b\n".to_string(), "tail\n".to_string()];
        let h = sample_hunk(vec![hl(HunkLineKind::Context, "a\tb\n")]);

        assert!(match_hunk_at(&target, &h, 0, 0, true, false).is_ok());
        assert!(match_hunk_at(&target, &h, 0, 0, false, false).is_err());
    }

    #[test]
    fn test_locate_hunk_position_finds_shifted_match() {
        let target = vec!["x\n".to_string(), "target\n".to_string(), "y\n".to_string()];
        let h = sample_hunk(vec![hl(HunkLineKind::Context, "target\n")]);
        let pos = locate_hunk_position(&target, &h, 0, 0, false, false).unwrap();
        assert_eq!(pos, 1);
    }

    #[test]
    fn test_apply_single_hunk_replaces_lines() {
        let mut target = vec!["old\n".to_string(), "tail\n".to_string()];
        let h = Hunk {
            old_start: 1,
            old_len: 1,
            new_start: 1,
            new_len: 1,
            header_suffix: None,
            lines: vec![
                hl(HunkLineKind::Remove, "old\n"),
                hl(HunkLineKind::Add, "new\n"),
            ],
        };
        apply_single_hunk(&mut target, &h, 0, false).unwrap();
        assert_eq!(target, vec!["new\n".to_string(), "tail\n".to_string()]);
    }

    #[test]
    fn test_apply_single_hunk_ifdef_wraps_block() {
        let mut target = vec!["old\n".to_string()];
        let h = Hunk {
            old_start: 1,
            old_len: 1,
            new_start: 1,
            new_len: 1,
            header_suffix: None,
            lines: vec![
                hl(HunkLineKind::Remove, "old\n"),
                hl(HunkLineKind::Add, "new\n"),
            ],
        };
        apply_single_hunk_ifdef(&mut target, &h, 0, "FEATURE_X").unwrap();
        assert_eq!(target[0], "#ifndef FEATURE_X\n");
        assert_eq!(target[2], "#else\n");
        assert_eq!(target[3], "new\n");
        assert_eq!(target[4], "#endif\n");
    }

    #[test]
    fn test_backup_path_for_simple_and_disabled() {
        let cfg_disabled = PatchConfig {
            input_patch: None,
            origfile: None,
            output: None,
            dry_run: false,
            strip: None,
            reverse: false,
            reverse_specified: false,
            fuzz: None,
            backup: false,
            backup_suffix: None,
            backup_prefix: None,
            basename_prefix: None,
            version_control: None,
            backup_if_mismatch: false,
            no_backup_if_mismatch: false,
            merge: false,
            force: false,
            batch: false,
            noreverse: false,
            reject_file: None,
            directory: None,
            patch_get: None,
            set_time: false,
            set_utc: false,
            verbose: false,
            quiet: false,
            format_hint: None,
            read_only: None,
            reject_format: None,
            ifdef_name: None,
            remove_empty_files: false,
            ignore_whitespace: false,
            quoting_style: None,
            follow_symlinks: false,
            binary: false,
            posix: false,
        };
        assert_eq!(
            backup_path_for(Path::new("foo.txt"), &cfg_disabled, false).unwrap(),
            PathBuf::new()
        );

        let mut cfg_simple = cfg_disabled.clone();
        cfg_simple.backup = true;
        cfg_simple.backup_suffix = Some(".orig".to_string());
        let b = backup_path_for(Path::new("foo.txt"), &cfg_simple, false).unwrap();
        assert_eq!(b, PathBuf::from("./foo.txt.orig"));
    }
}

#[cfg(test)]
fn integration_test_lock() -> &'static std::sync::Mutex<()> {
    use std::sync::{Mutex, OnceLock};

    static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
    LOCK.get_or_init(|| Mutex::new(()))
}

#[cfg(test)]
mod integration_flow_tests {
    use super::*;
    struct TempDirGuard {
        path: PathBuf,
    }

    impl TempDirGuard {
        fn new(tag: &str) -> Self {
            let nanos = SystemTime::now()
                .duration_since(SystemTime::UNIX_EPOCH)
                .map(|d| d.as_nanos())
                .unwrap_or(0);
            let path = std::env::temp_dir().join(format!("kylin-patch-it-{}-{}-{}", tag, std::process::id(), nanos));
            fs::create_dir_all(&path).expect("create temp dir");
            Self { path }
        }

        fn path(&self) -> &Path {
            &self.path
        }
    }

    impl Drop for TempDirGuard {
        fn drop(&mut self) {
            let _ = fs::remove_dir_all(&self.path);
        }
    }

    fn write_text(path: &Path, content: &str) {
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent).expect("create parent");
        }
        fs::write(path, content).expect("write file");
    }

    fn read_text(path: &Path) -> String {
        fs::read_to_string(path).expect("read file")
    }

    fn cfg_with_patch(patch_path: &Path) -> PatchConfig {
        PatchConfig {
            input_patch: Some(patch_path.to_path_buf()),
            origfile: None,
            output: None,
            dry_run: false,
            strip: None,
            reverse: false,
            reverse_specified: false,
            fuzz: None,
            backup: false,
            backup_suffix: None,
            backup_prefix: None,
            basename_prefix: None,
            version_control: None,
            backup_if_mismatch: true,
            no_backup_if_mismatch: false,
            merge: false,
            force: true,
            batch: false,
            noreverse: false,
            reject_file: None,
            directory: None,
            patch_get: None,
            set_time: false,
            set_utc: false,
            verbose: false,
            quiet: true,
            format_hint: None,
            read_only: None,
            reject_format: None,
            ifdef_name: None,
            remove_empty_files: false,
            ignore_whitespace: false,
            quoting_style: Some("literal".to_string()),
            follow_symlinks: false,
            binary: false,
            posix: false,
        }
    }

    #[test]
    fn it_applies_unified_patch_main_flow() {
        let _guard = super::integration_test_lock().lock().unwrap_or_else(|e| e.into_inner());
        let td = TempDirGuard::new("unified");
        let old_cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
        std::env::set_current_dir(td.path()).expect("chdir temp");

        write_text(Path::new("t.txt"), "a\nb\n");
        write_text(
            Path::new("p.diff"),
            "--- t.txt\n+++ t.txt\n@@ -1,2 +1,2 @@\n-a\n+x\n b\n",
        );
        let cfg = cfg_with_patch(Path::new("p.diff"));
        let exit = apply_patches(&cfg);

        std::env::set_current_dir(old_cwd).expect("restore cwd");
        assert_eq!(exit, PatchExit::Ok);
        assert_eq!(read_text(&td.path().join("t.txt")), "x\nb\n");
    }

    #[test]
    fn it_applies_context_patch() {
        let _guard = super::integration_test_lock().lock().unwrap_or_else(|e| e.into_inner());
        let td = TempDirGuard::new("context");
        let old_cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
        std::env::set_current_dir(td.path()).expect("chdir temp");

        write_text(Path::new("a"), "a
");
        write_text(
            Path::new("ab.diff"),
            "*** a
--- b
***************
*** 1 ****
- a
--- 1 ----
+ x
",
        );
        let cfg = cfg_with_patch(Path::new("ab.diff"));
        let exit = apply_patches(&cfg);

        std::env::set_current_dir(old_cwd).expect("restore cwd");
        assert_eq!(exit, PatchExit::Ok);
        assert_eq!(read_text(&td.path().join("a")), "x
");
    }

    #[test]
    fn it_applies_normal_patch() {
        let _guard = super::integration_test_lock().lock().unwrap_or_else(|e| e.into_inner());
        let td = TempDirGuard::new("normal");
        let old_cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
        std::env::set_current_dir(td.path()).expect("chdir temp");

        write_text(Path::new("f"), "a\nold\nc\n");
        write_text(Path::new("n.diff"), "2c2\n< old\n---\n> new\n");

        let mut cfg = cfg_with_patch(Path::new("n.diff"));
        cfg.origfile = Some(PathBuf::from("f"));
        cfg.format_hint = Some(DiffFormatHint::Normal);
        let exit = apply_patches(&cfg);

        std::env::set_current_dir(old_cwd).expect("restore cwd");
        assert_eq!(exit, PatchExit::Ok);
        assert_eq!(read_text(&td.path().join("f")), "a\nnew\nc\n");
    }

    #[test]
    fn it_honors_dry_run() {
        let _guard = super::integration_test_lock().lock().unwrap_or_else(|e| e.into_inner());
        let td = TempDirGuard::new("dryrun");
        let old_cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
        std::env::set_current_dir(td.path()).expect("chdir temp");

        write_text(Path::new("f"), "one\n");
        write_text(Path::new("p.diff"), "--- f\n+++ f\n@@ -1 +1 @@\n-one\n+two\n");

        let mut cfg = cfg_with_patch(Path::new("p.diff"));
        cfg.dry_run = true;
        let exit = apply_patches(&cfg);

        std::env::set_current_dir(old_cwd).expect("restore cwd");
        assert_eq!(exit, PatchExit::Ok);
        assert_eq!(read_text(&td.path().join("f")), "one\n");
    }

    #[test]
    fn it_supports_reverse_mode() {
        let _guard = super::integration_test_lock().lock().unwrap_or_else(|e| e.into_inner());
        let td = TempDirGuard::new("reverse");
        let old_cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
        std::env::set_current_dir(td.path()).expect("chdir temp");

        write_text(Path::new("f"), "one\n");
        write_text(Path::new("p.diff"), "--- f\n+++ f\n@@ -1 +1 @@\n-one\n+two\n");

        let cfg = cfg_with_patch(Path::new("p.diff"));
        assert_eq!(apply_patches(&cfg), PatchExit::Ok);

        let mut rev = cfg_with_patch(Path::new("p.diff"));
        rev.reverse = true;
        rev.reverse_specified = true;
        let exit = apply_patches(&rev);

        std::env::set_current_dir(old_cwd).expect("restore cwd");
        assert_eq!(exit, PatchExit::Ok);
        assert_eq!(read_text(&td.path().join("f")), "one\n");
    }

    #[test]
    fn it_uses_directory_option() {
        let _guard = super::integration_test_lock().lock().unwrap_or_else(|e| e.into_inner());
        let td = TempDirGuard::new("directory");
        let old_cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
        write_text(&td.path().join("sub/f"), "a\n");
        write_text(&td.path().join("sub/p.diff"), "--- f\n+++ f\n@@ -1 +1 @@\n-a\n+b\n");

        let mut cfg = cfg_with_patch(&td.path().join("sub/p.diff"));
        cfg.directory = Some(td.path().join("sub"));
        let exit = apply_patches(&cfg);

        std::env::set_current_dir(old_cwd).expect("restore cwd");
        assert_eq!(exit, PatchExit::Ok);
        assert_eq!(read_text(&td.path().join("sub/f")), "b\n");
    }

    #[test]
    fn it_handles_git_style_default_strip() {
        let _guard = super::integration_test_lock().lock().unwrap_or_else(|e| e.into_inner());
        let td = TempDirGuard::new("gitstrip");
        let old_cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
        std::env::set_current_dir(td.path()).expect("chdir temp");

        write_text(Path::new("dir/f"), "a\n");
        write_text(
            Path::new("p.diff"),
            "diff --git a/dir/f b/dir/f\n--- a/dir/f\n+++ b/dir/f\n@@ -1 +1 @@\n-a\n+b\n",
        );

        let cfg = cfg_with_patch(Path::new("p.diff"));
        let exit = apply_patches(&cfg);

        std::env::set_current_dir(old_cwd).expect("restore cwd");
        assert_eq!(exit, PatchExit::Ok);
        assert_eq!(read_text(&td.path().join("dir/f")), "b\n");
    }

    #[test]
    fn it_honors_explicit_strip_count() {
        let _guard = super::integration_test_lock().lock().unwrap_or_else(|e| e.into_inner());
        let td = TempDirGuard::new("stripn");
        let old_cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
        std::env::set_current_dir(td.path()).expect("chdir temp");

        write_text(Path::new("f"), "a\n");
        write_text(
            Path::new("p.diff"),
            "--- x/y/f\n+++ x/y/f\n@@ -1 +1 @@\n-a\n+b\n",
        );

        let mut cfg = cfg_with_patch(Path::new("p.diff"));
        cfg.strip = Some(2);
        let exit = apply_patches(&cfg);

        std::env::set_current_dir(old_cwd).expect("restore cwd");
        assert_eq!(exit, PatchExit::Ok);
        assert_eq!(read_text(&td.path().join("f")), "b\n");
    }

    #[test]
    fn it_writes_reject_file_on_failed_hunk() {
        let _guard = super::integration_test_lock().lock().unwrap_or_else(|e| e.into_inner());
        let td = TempDirGuard::new("reject");
        let old_cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
        std::env::set_current_dir(td.path()).expect("chdir temp");

        write_text(Path::new("f"), "one\n");
        write_text(Path::new("p.diff"), "--- f\n+++ f\n@@ -1 +1 @@\n-two\n+three\n");

        let mut cfg = cfg_with_patch(Path::new("p.diff"));
        cfg.reject_file = Some(PathBuf::from("rej.out"));
        let exit = apply_patches(&cfg);

        std::env::set_current_dir(old_cwd).expect("restore cwd");
        assert_eq!(exit, PatchExit::SomeFailed);
        let rej = td.path().join("rej.out");
        assert!(rej.exists());
        assert!(!read_text(&rej).is_empty());
    }

    #[test]
    fn it_can_copy_orig_to_output_on_empty_patch() {
        let _guard = super::integration_test_lock().lock().unwrap_or_else(|e| e.into_inner());
        let td = TempDirGuard::new("emptycopy");

        write_text(&td.path().join("orig.txt"), "hello\nworld\n");
        write_text(&td.path().join("empty.diff"), "");

        let mut cfg = cfg_with_patch(&td.path().join("empty.diff"));
        cfg.origfile = Some(td.path().join("orig.txt"));
        cfg.output = Some(td.path().join("out.txt"));
        let exit = apply_patches(&cfg);

        assert_eq!(exit, PatchExit::Ok);
        assert_eq!(read_text(&td.path().join("out.txt")), "hello\nworld\n");
    }

    #[test]
    fn it_respects_ignore_whitespace_matching() {
        let _guard = super::integration_test_lock().lock().unwrap_or_else(|e| e.into_inner());
        let td = TempDirGuard::new("ws");
        let old_cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
        std::env::set_current_dir(td.path()).expect("chdir temp");

        write_text(Path::new("f"), "a  b\n");
        write_text(Path::new("p.diff"), "--- f\n+++ f\n@@ -1 +1 @@\n-a\tb\n+ok\n");

        let mut cfg = cfg_with_patch(Path::new("p.diff"));
        cfg.ignore_whitespace = true;
        let exit = apply_patches(&cfg);

        std::env::set_current_dir(old_cwd).expect("restore cwd");
        assert_eq!(exit, PatchExit::Ok);
        assert_eq!(read_text(&td.path().join("f")), "ok\n");
    }

    #[test]
    fn it_can_remove_empty_files_option() {
        let _guard = super::integration_test_lock().lock().unwrap_or_else(|e| e.into_inner());
        let td = TempDirGuard::new("emptyrm");
        let old_cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
        std::env::set_current_dir(td.path()).expect("chdir temp");

        write_text(Path::new("f"), "one\n");
        write_text(Path::new("p.diff"), "--- f\n+++ f\n@@ -1 +0,0 @@\n-one\n");

        let mut cfg = cfg_with_patch(Path::new("p.diff"));
        cfg.remove_empty_files = true;
        let exit = apply_patches(&cfg);

        std::env::set_current_dir(old_cwd).expect("restore cwd");
        assert_eq!(exit, PatchExit::Ok);
        assert!(!td.path().join("f").exists());
    }

    #[test]
    fn it_reports_trouble_for_garbage_patch() {
        let _guard = super::integration_test_lock().lock().unwrap_or_else(|e| e.into_inner());
        let td = TempDirGuard::new("garbage");
        write_text(&td.path().join("g.diff"), "this is not a patch\n");

        let cfg = cfg_with_patch(&td.path().join("g.diff"));
        let exit = apply_patches(&cfg);

        assert_eq!(exit, PatchExit::Trouble);
    }

    #[test]
    fn it_reads_patch_from_stdin_when_no_input_file_is_given() {
        let _guard = super::integration_test_lock().lock().unwrap_or_else(|e| e.into_inner());
        let td = TempDirGuard::new("stdin");
        let old_cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
        std::env::set_current_dir(td.path()).expect("chdir temp");

        write_text(Path::new("f"), "old\n");
        let patch = "--- f\n+++ f\n@@ -1 +1 @@\n-old\n+new\n";
        let stdin_path = td.path().join("stdin.txt");
        write_text(&stdin_path, patch);
        let stdin_file = fs::File::open(&stdin_path).expect("open stdin source");
        let old_stdin = io::stdin();
        let old_fd = {
            #[cfg(unix)]
            {
                use std::os::fd::AsRawFd;
                let dup = unsafe { libc::dup(old_stdin.as_raw_fd()) };
                assert!(dup >= 0, "dup stdin failed");
                dup
            }
            #[cfg(not(unix))]
            {
                -1
            }
        };
        #[cfg(unix)]
        {
            use std::os::fd::AsRawFd;
            let rc = unsafe { libc::dup2(stdin_file.as_raw_fd(), old_stdin.as_raw_fd()) };
            assert!(rc >= 0, "redirect stdin failed");
        }

        let mut cfg = cfg_with_patch(Path::new("unused.diff"));
        cfg.input_patch = None;
        let exit = apply_patches(&cfg);

        #[cfg(unix)]
        {
            use std::os::fd::AsRawFd;
            let rc = unsafe { libc::dup2(old_fd, old_stdin.as_raw_fd()) };
            assert!(rc >= 0, "restore stdin failed");
            unsafe { libc::close(old_fd) };
        }
        std::env::set_current_dir(old_cwd).expect("restore cwd");
        assert_eq!(exit, PatchExit::Ok);
        assert_eq!(read_text(&td.path().join("f")), "new\n");
    }

    #[test]
    fn it_synthesizes_headers_for_origfile_unified_hunks() {
        let _guard = super::integration_test_lock().lock().unwrap_or_else(|e| e.into_inner());
        let td = TempDirGuard::new("origfile-unified");
        write_text(&td.path().join("f"), "one\n");
        write_text(&td.path().join("p.diff"), "@@ -1 +1 @@\n-one\n+two\n");

        let mut cfg = cfg_with_patch(&td.path().join("p.diff"));
        cfg.origfile = Some(td.path().join("f"));
        let exit = apply_patches(&cfg);

        assert_eq!(exit, PatchExit::Ok);
        assert_eq!(read_text(&td.path().join("f")), "two\n");
    }

    #[test]
    fn it_strips_crlf_from_patch_headers_when_not_binary() {
        let _guard = super::integration_test_lock().lock().unwrap_or_else(|e| e.into_inner());
        let td = TempDirGuard::new("crlf-strip");
        let old_cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
        std::env::set_current_dir(td.path()).expect("chdir temp");

        write_text(Path::new("f"), "old\n");
        write_text(
            Path::new("p.diff"),
            "--- f\r\n+++ f\r\n@@ -1 +1 @@\r\n-old\r\n+new\r\n",
        );

        let cfg = cfg_with_patch(Path::new("p.diff"));
        let exit = apply_patches(&cfg);

        std::env::set_current_dir(old_cwd).expect("restore cwd");
        assert_eq!(exit, PatchExit::Ok);
        assert_eq!(read_text(&td.path().join("f")), "new\n");
    }

    #[test]
    fn it_applies_explicit_unified_hint_to_hunk_only_patch() {
        let _guard = super::integration_test_lock().lock().unwrap_or_else(|e| e.into_inner());
        let td = TempDirGuard::new("forced-unified");
        write_text(&td.path().join("f"), "left\n");
        write_text(&td.path().join("p.diff"), "@@ -1 +1 @@\n-left\n+right\n");

        let mut cfg = cfg_with_patch(&td.path().join("p.diff"));
        cfg.origfile = Some(td.path().join("f"));
        cfg.format_hint = Some(DiffFormatHint::Unified);
        let exit = apply_patches(&cfg);

        assert_eq!(exit, PatchExit::Ok);
        assert_eq!(read_text(&td.path().join("f")), "right\n");
    }
}

#[cfg(test)]
mod integration_git_tests {
    use super::*;
    fn mktemp(tag: &str) -> PathBuf {
        let nanos = SystemTime::now()
            .duration_since(SystemTime::UNIX_EPOCH)
            .map(|d| d.as_nanos())
            .unwrap_or(0);
        let p = std::env::temp_dir().join(format!("kylin-patch-it2-{}-{}-{}", tag, std::process::id(), nanos));
        fs::create_dir_all(&p).expect("mkdir");
        p
    }

    fn write_file(path: &Path, content: &str) {
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent).expect("mkdir parent");
        }
        fs::write(path, content).expect("write");
    }

    fn base_cfg(patch_path: PathBuf) -> PatchConfig {
        PatchConfig {
            input_patch: Some(patch_path),
            origfile: None,
            output: None,
            dry_run: false,
            strip: None,
            reverse: false,
            reverse_specified: false,
            fuzz: None,
            backup: false,
            backup_suffix: None,
            backup_prefix: None,
            basename_prefix: None,
            version_control: None,
            backup_if_mismatch: true,
            no_backup_if_mismatch: false,
            merge: false,
            force: true,
            batch: false,
            noreverse: false,
            reject_file: None,
            directory: None,
            patch_get: None,
            set_time: false,
            set_utc: false,
            verbose: false,
            quiet: true,
            format_hint: None,
            read_only: None,
            reject_format: None,
            ifdef_name: None,
            remove_empty_files: false,
            ignore_whitespace: false,
            quoting_style: Some("literal".to_string()),
            follow_symlinks: false,
            binary: false,
            posix: false,
        }
    }

    #[test]
    fn it_handles_new_file_from_dev_null() {
        let _g = super::integration_test_lock().lock().unwrap_or_else(|e| e.into_inner());
        let td = mktemp("newfile");
        let old_cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
        std::env::set_current_dir(&td).expect("chdir");

        write_file(
            &td.join("p.diff"),
            "--- /dev/null\n+++ n.txt\n@@ -0,0 +1,2 @@\n+hello\n+world\n",
        );
        let exit = apply_patches(&base_cfg(td.join("p.diff")));

        std::env::set_current_dir(old_cwd).expect("restore");
        assert_eq!(exit, PatchExit::Ok);
        assert_eq!(fs::read_to_string(td.join("n.txt")).expect("read"), "hello\nworld\n");
        let _ = fs::remove_dir_all(&td);
    }

    #[test]
    fn it_skips_dangerous_relative_path() {
        let _g = super::integration_test_lock().lock().unwrap_or_else(|e| e.into_inner());
        let td = mktemp("danger");
        write_file(
            &td.join("p.diff"),
            "--- ../../evil\n+++ ../../evil\n@@ -1 +1 @@\n-a\n+b\n",
        );
        let exit = apply_patches(&base_cfg(td.join("p.diff")));
        assert_eq!(exit, PatchExit::SomeFailed);
        let _ = fs::remove_dir_all(&td);
    }

    #[test]
    fn it_reports_missing_target_file() {
        let _g = super::integration_test_lock().lock().unwrap_or_else(|e| e.into_inner());
        let td = mktemp("missing");
        write_file(
            &td.join("p.diff"),
            "--- notfound.txt\n+++ notfound.txt\n@@ -1 +1 @@\n-a\n+b\n",
        );
        let exit = apply_patches(&base_cfg(td.join("p.diff")));
        assert_eq!(exit, PatchExit::SomeFailed);
        let _ = fs::remove_dir_all(&td);
    }

    #[test]
    fn it_handles_git_binary_patch_marker() {
        let _g = super::integration_test_lock().lock().unwrap_or_else(|e| e.into_inner());
        let td = mktemp("gitbin");
        write_file(
            &td.join("p.diff"),
            "diff --git a/bin.dat b/bin.dat\nindex 1111111..2222222 100644\nGIT binary patch\n",
        );
        let exit = apply_patches(&base_cfg(td.join("p.diff")));
        assert_eq!(exit, PatchExit::SomeFailed);
        let _ = fs::remove_dir_all(&td);
    }

    #[test]
    fn it_parses_rename_only_patch_block() {
        let _g = super::integration_test_lock().lock().unwrap_or_else(|e| e.into_inner());
        let patch = "diff --git a/old.txt b/new.txt\nrename from old.txt\nrename to new.txt\n";
        let mut r = BufReader::new(Cursor::new(patch.as_bytes().to_vec()));
        let (items, _) = parse_unified_patches(&mut r).expect("parse");
        assert_eq!(items.len(), 1);
        assert_eq!(items[0].rename_from.as_deref(), Some("old.txt"));
        assert_eq!(items[0].rename_to.as_deref(), Some("new.txt"));
    }

    #[test]
    fn it_parses_copy_and_binary_header_blocks() {
        let _g = super::integration_test_lock().lock().unwrap_or_else(|e| e.into_inner());
        let patch = "diff --git a/src.bin b/dst.bin\ncopy from src.bin\ncopy to dst.bin\nBinary files a/src.bin and b/dst.bin differ\n";
        let mut r = BufReader::new(Cursor::new(patch.as_bytes().to_vec()));
        let (items, _) = parse_unified_patches(&mut r).expect("parse");
        assert_eq!(items.len(), 1);
        assert_eq!(items[0].copy_from.as_deref(), Some("src.bin"));
        assert_eq!(items[0].copy_to.as_deref(), Some("dst.bin"));
        assert_eq!(items[0].old_path.as_deref(), Some("a/src.bin"));
        assert_eq!(items[0].new_path.as_deref(), Some("b/dst.bin"));
    }

    #[test]
    fn it_marks_malformed_hunk_in_parser() {
        let _g = super::integration_test_lock().lock().unwrap_or_else(|e| e.into_inner());
        let patch = "--- a\n+++ a\n@@ bad-header @@\n+a\n";
        let mut r = BufReader::new(Cursor::new(patch.as_bytes().to_vec()));
        let (items, _) = parse_unified_patches(&mut r).expect("parse");
        assert_eq!(items.len(), 1);
        assert!(items[0].malformed_line.is_some());
        assert!(items[0].malformed_text.as_deref().unwrap_or("").contains("@@ bad-header @@"));
    }
}

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

    fn cfg_for_reject(fmt: Option<RejectFormat>) -> PatchConfig {
        PatchConfig {
            input_patch: None,
            origfile: None,
            output: None,
            dry_run: false,
            strip: None,
            reverse: false,
            reverse_specified: false,
            fuzz: None,
            backup: false,
            backup_suffix: None,
            backup_prefix: None,
            basename_prefix: None,
            version_control: None,
            backup_if_mismatch: true,
            no_backup_if_mismatch: false,
            merge: false,
            force: true,
            batch: false,
            noreverse: false,
            reject_file: None,
            directory: None,
            patch_get: None,
            set_time: false,
            set_utc: false,
            verbose: false,
            quiet: true,
            format_hint: None,
            read_only: None,
            reject_format: fmt,
            ifdef_name: None,
            remove_empty_files: false,
            ignore_whitespace: false,
            quoting_style: Some("literal".to_string()),
            follow_symlinks: false,
            binary: false,
            posix: false,
        }
    }

    fn mktemp(tag: &str) -> PathBuf {
        let nanos = SystemTime::now()
            .duration_since(SystemTime::UNIX_EPOCH)
            .map(|d| d.as_nanos())
            .unwrap_or(0);
        let p = std::env::temp_dir().join(format!("kylin-patch-aux-{}-{}-{}", tag, std::process::id(), nanos));
        fs::create_dir_all(&p).expect("mkdir");
        p
    }

    fn sample_hunk() -> Hunk {
        Hunk {
            old_start: 1,
            old_len: 1,
            new_start: 1,
            new_len: 1,
            header_suffix: Some(" fn".to_string()),
            lines: vec![
                HunkLine { kind: HunkLineKind::Remove, text: "old\n".to_string() },
                HunkLine { kind: HunkLineKind::Add, text: "new\n".to_string() },
            ],
        }
    }

    #[test]
    fn parse_timestamp_from_header_parses_offsets_and_local() {
        let t1 = parse_timestamp_from_header("2002-02-21 23:30:39-0800Z", true);
        assert!(t1.is_some());

        let t2 = parse_timestamp_from_header("2002-02-21 23:30:39.123+0800Z", true);
        assert!(t2.is_some());

        let t3 = parse_timestamp_from_header("2002-02-21 23:30", false);
        assert!(t3.is_some());

        assert!(parse_timestamp_from_header("", true).is_none());
    }

    #[test]
    fn parse_ed_addr_parses_ranges() {
        assert_eq!(parse_ed_addr("12,15c"), ((12, 3), "c"));
        assert_eq!(parse_ed_addr("3d"), ((3, 1), "d"));
        assert_eq!(parse_ed_addr("5a"), ((5, 1), "a"));
    }

    #[test]
    fn apply_ed_script_updates_file() {
        let td = mktemp("ed");
        let f = td.join("f.txt");
        fs::write(&f, "a\nb\nc\n").expect("write");

        let script = "2c\nX\n.\n3a\nY\n.\n";
        let mut reader = BufReader::new(Cursor::new(script.as_bytes().to_vec()));
        let mut cfg = cfg_for_reject(None);
        cfg.origfile = Some(f.clone());
        apply_ed_script(&mut reader, &f, &cfg).expect("apply ed");

        let got = fs::read_to_string(&f).expect("read");
        assert_eq!(got, "a\nX\nc\nY\n");

        let _ = fs::remove_dir_all(&td);
    }

    #[test]
    fn collect_failed_hunks_returns_first_failure() {
        let orig = vec!["a\n".to_string()];
        let h_ok = Hunk {
            old_start: 1,
            old_len: 1,
            new_start: 1,
            new_len: 1,
            header_suffix: None,
            lines: vec![
                HunkLine { kind: HunkLineKind::Remove, text: "a\n".to_string() },
                HunkLine { kind: HunkLineKind::Add, text: "b\n".to_string() },
            ],
        };
        let h_bad = Hunk {
            old_start: 5,
            old_len: 1,
            new_start: 5,
            new_len: 1,
            header_suffix: None,
            lines: vec![HunkLine { kind: HunkLineKind::Context, text: "z\n".to_string() }],
        };

        let failed = collect_failed_hunks_for_reject(&orig, &[h_ok, h_bad.clone()], 0, false, false);
        assert_eq!(failed.len(), 1);
        assert_eq!(failed[0].old_start, h_bad.old_start);
    }

    #[test]
    fn write_reject_hunks_writes_unified_and_context() {
        let td = mktemp("rejectfmt");
        let target = td.join("t.txt");
        fs::write(&target, "old\n").expect("write");
        let hunks = vec![sample_hunk()];

        let rej_u = td.join("u.rej");
        write_reject_hunks(
            &cfg_for_reject(Some(RejectFormat::Unified)),
            &rej_u,
            false,
            &target,
            Some("Index: t.txt"),
            Some(RejectFormat::Unified),
            Some("a/t.txt"),
            Some("b/t.txt"),
            None,
            None,
            &hunks,
        )
        .expect("write unified");
        let u = fs::read_to_string(&rej_u).expect("read u");
        assert!(u.contains("--- a/t.txt"));
        assert!(u.contains("+++ b/t.txt"));
        assert!(u.contains("@@ -1 +1 @@ fn"));

        let rej_c = td.join("c.rej");
        write_reject_hunks(
            &cfg_for_reject(Some(RejectFormat::Context)),
            &rej_c,
            false,
            &target,
            Some("Index: t.txt"),
            Some(RejectFormat::Context),
            Some("a/t.txt"),
            Some("b/t.txt"),
            None,
            None,
            &hunks,
        )
        .expect("write context");
        let c = fs::read_to_string(&rej_c).expect("read c");
        assert!(c.contains("*** a/t.txt"));
        assert!(c.contains("--- b/t.txt"));
        assert!(c.contains("***************"));

        let _ = fs::remove_dir_all(&td);
    }
}

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

    fn mktemp(tag: &str) -> PathBuf {
        let nanos = SystemTime::now()
            .duration_since(SystemTime::UNIX_EPOCH)
            .map(|d| d.as_nanos())
            .unwrap_or(0);
        let p = std::env::temp_dir().join(format!("kylin-patch-perm-{}-{}-{}", tag, std::process::id(), nanos));
        fs::create_dir_all(&p).expect("mkdir");
        p
    }

    fn cfg(patch_path: &Path) -> PatchConfig {
        PatchConfig {
            input_patch: Some(patch_path.to_path_buf()),
            origfile: None,
            output: None,
            dry_run: false,
            strip: None,
            reverse: false,
            reverse_specified: false,
            fuzz: None,
            backup: false,
            backup_suffix: None,
            backup_prefix: None,
            basename_prefix: None,
            version_control: None,
            backup_if_mismatch: true,
            no_backup_if_mismatch: false,
            merge: false,
            force: true,
            batch: false,
            noreverse: false,
            reject_file: None,
            directory: None,
            patch_get: None,
            set_time: false,
            set_utc: false,
            verbose: false,
            quiet: true,
            format_hint: None,
            read_only: None,
            reject_format: None,
            ifdef_name: None,
            remove_empty_files: false,
            ignore_whitespace: false,
            quoting_style: Some("literal".to_string()),
            follow_symlinks: false,
            binary: false,
            posix: false,
        }
    }

    #[test]
    fn read_only_fail_produces_reject() {
        let _g = super::integration_test_lock().lock().unwrap_or_else(|e| e.into_inner());
        let td = mktemp("ro-fail");
        let old_cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
        std::env::set_current_dir(&td).expect("chdir");

        fs::write("f", "a\n").expect("write");
        fs::write("p.diff", "--- f\n+++ f\n@@ -1 +1 @@\n-a\n+b\n").expect("write diff");
        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            let mut perm = fs::metadata("f").expect("meta").permissions();
            perm.set_mode(0o444);
            fs::set_permissions("f", perm).expect("chmod");
        }

        let mut c = cfg(Path::new("p.diff"));
        c.read_only = Some(ReadOnlyMode::Fail);
        let exit = apply_patches(&c);

        std::env::set_current_dir(old_cwd).expect("restore");
        assert_eq!(exit, PatchExit::SomeFailed);
        assert!(td.join("f.rej").exists());
        let _ = fs::remove_dir_all(&td);
    }

    #[test]
    fn read_only_ignore_allows_patch() {
        let _g = super::integration_test_lock().lock().unwrap_or_else(|e| e.into_inner());
        let td = mktemp("ro-ignore");
        let old_cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
        std::env::set_current_dir(&td).expect("chdir");

        fs::write("f", "a\n").expect("write");
        fs::write("p.diff", "--- f\n+++ f\n@@ -1 +1 @@\n-a\n+b\n").expect("write diff");
        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            let mut perm = fs::metadata("f").expect("meta").permissions();
            perm.set_mode(0o444);
            fs::set_permissions("f", perm).expect("chmod");
        }

        let mut c = cfg(Path::new("p.diff"));
        c.read_only = Some(ReadOnlyMode::Ignore);
        let exit = apply_patches(&c);

        std::env::set_current_dir(old_cwd).expect("restore");
        assert_eq!(exit, PatchExit::Ok);
        assert_eq!(fs::read_to_string(td.join("f")).expect("read"), "b\n");
        let _ = fs::remove_dir_all(&td);
    }

    #[test]
    fn backup_file_created_when_enabled() {
        let _g = super::integration_test_lock().lock().unwrap_or_else(|e| e.into_inner());
        let td = mktemp("backup");
        let old_cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
        std::env::set_current_dir(&td).expect("chdir");

        fs::write("f", "a\n").expect("write");
        fs::write("p.diff", "--- f\n+++ f\n@@ -1 +1 @@\n-a\n+b\n").expect("write diff");

        let mut c = cfg(Path::new("p.diff"));
        c.backup = true;
        c.backup_suffix = Some(".orig".to_string());
        let exit = apply_patches(&c);

        std::env::set_current_dir(old_cwd).expect("restore");
        assert_eq!(exit, PatchExit::Ok);
        assert!(td.join("f.orig").exists() || td.join("./f.orig").exists());
        let _ = fs::remove_dir_all(&td);
    }

    #[test]
    fn delete_missing_file_without_force_is_failed() {
        let _g = super::integration_test_lock().lock().unwrap_or_else(|e| e.into_inner());
        let td = mktemp("del-missing");
        let old_cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
        std::env::set_current_dir(&td).expect("chdir");

        fs::write("p.diff", "--- f\n+++ /dev/null\n@@ -1 +0,0 @@\n-a\n").expect("write diff");
        let mut c = cfg(Path::new("p.diff"));
        c.force = false;
        let exit = apply_patches(&c);

        std::env::set_current_dir(old_cwd).expect("restore");
        assert_eq!(exit, PatchExit::SomeFailed);
        let _ = fs::remove_dir_all(&td);
    }

    #[test]
    fn symlink_target_refused_when_no_follow() {
        let _g = super::integration_test_lock().lock().unwrap_or_else(|e| e.into_inner());
        let td = mktemp("symlink");
        let old_cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
        std::env::set_current_dir(&td).expect("chdir");

        fs::write("real", "a\n").expect("write");
        #[cfg(unix)]
        {
            std::os::unix::fs::symlink("real", "lnk").expect("symlink");
        }
        fs::write("p.diff", "--- lnk\n+++ lnk\n@@ -1 +1 @@\n-a\n+b\n").expect("write diff");
        let c = cfg(Path::new("p.diff"));
        let exit = apply_patches(&c);

        std::env::set_current_dir(old_cwd).expect("restore");
        assert_eq!(exit, PatchExit::SomeFailed);
        let _ = fs::remove_dir_all(&td);
    }

    #[test]
    fn empty_patch_without_orig_output_is_trouble() {
        let _g = super::integration_test_lock().lock().unwrap_or_else(|e| e.into_inner());
        let td = mktemp("empty");
        fs::write(td.join("e.diff"), "").expect("write");
        let c = cfg(&td.join("e.diff"));
        let exit = apply_patches(&c);
        assert_eq!(exit, PatchExit::Trouble);
        let _ = fs::remove_dir_all(&td);
    }
}
