//! F-260619-03 Phase A/B/C: AI 工具文件访问授权目录白名单模块
//!
//! 从 `state.rs` 抽离的授权相关类型/函数:`AllowedDirs` 三档白名单
//! (persistent / session / once)、`PathAuthDecision` 三态决策、
//! `check_path_authorization` 词法层预校验、`is_in_system_blacklist`
//! 系统敏感目录黑名单,以及 `strip_verbatim` / `best_effort_canonicalize`
//! / `paths_eq` 辅助函数。
//!
//! 注:`workspace_root_path` 仍保留在 `state.rs`(旧 `.trash` 迁移仍需);
//! 本模块测试内定义本地副本避免跨文件耦合。

use std::collections::HashSet;
use std::path::{Path, PathBuf};

/// F-260619-03 Phase A/B/C: AI 工具文件访问授权目录白名单
///
/// - Phase A:`persistent`(持久化白名单,从 Settings KV `allowed_dirs` 加载,
///   JSON 数组 `["E:/wk-lab/u-abc"]`)。`resolve_workspace_path` 校验时:
///   - 任一 persistent 或 session 目录 starts_with 命中即放行
///   - 无任何授权时全部拒绝(引导用户绑定项目或授权目录)
/// - Phase B:`session`(进程级会话临时授权,弹窗"仅本次"写入;切换/新建/删除会话清空)。
///   单用户桌面应用 active_conversation_id 单全局模型,session 字段随 active 切换清空,
///   行为等价"当前活跃会话的临时授权"。handler 闭包(read lock 取快照)与
///   process_tool_calls 预校验均读此字段,两端一致。
/// - Phase C:黑名单(is_authorized 内,黑名单优先于白名单 — 命中即拒)。
#[derive(Debug, Clone, Default)]
pub struct AllowedDirs {
    /// 持久化授权目录(Settings KV `allowed_dirs`,JSON 字符串数组)。
    /// 已规范化(canonicalize 失败回退原字面量),便于 starts_with 精确比对。
    pub persistent: HashSet<PathBuf>,
    /// F-260619-03 Phase B: 会话级临时授权目录(弹窗"当前会话"选项写入,进程级内存)。
    /// 仅当前活跃会话生效,切换/新建/删除会话由 clear_session_allowed_dirs 清空(不落库)。
    /// handler 闭包与 process_tool_calls 预校验均读此字段,确保两端授权判定一致。
    pub session: HashSet<PathBuf>,
    /// 本次单次授权目录(弹窗"本次"选项写入)。单次工具执行放行后由 clear_once_allowed_dirs
    /// 清空(用完即弃,下次同路径再访问仍弹窗)。区别于 session(整会话有效)。
    /// 三档授权语义:once=本次单次 / session=当前会话 / persistent=始终落 KV。
    pub once: HashSet<PathBuf>,
}

impl AllowedDirs {
    /// Settings KV key:F-260619-03 Phase A 持久化白名单(JSON 字符串数组)
    pub const SETTINGS_KEY: &'static str = "allowed_dirs";

    /// 空白名单(方案①弱化 workspace_root:不再编译期硬塞开发机 CARGO_MANIFEST_DIR)。
    /// 分发后该路径指向编译机不存在的目录,硬塞反成脏白名单。
    /// 当前仅测试使用,生产环境由 reload_allowed_dirs 覆盖。
    #[cfg_attr(not(test), allow(dead_code))]
    pub fn default_with_root() -> Self {
        Self { persistent: HashSet::new(), session: HashSet::new(), once: HashSet::new() }
    }

    /// 取首个 persistent 授权目录,作为相对路径锚定基准。
    /// 无任何持久授权时返回 None,调用方应引导用户绑定项目。
    pub fn first_persistent_dir(&self) -> Option<&PathBuf> {
        self.persistent.iter().next()
    }

    /// 路径是否被授权:persistent 或 session 白名单任一 starts_with 命中即放行。
    ///
    /// **方案①弱化后 workspace_root 不再默认授权**(default_with_root 空,reload_allowed_dirs
    /// KV 未配时也不塞),首次访问工程根走弹窗三档授权。用户授权(always/session/once)后
    /// 落白名单,后续命中放行(动态白名单完整语义,F-260619-03 收尾)。
    ///
    /// **Phase C 黑名单优先**:即使白名单命中,若路径落入系统敏感目录(Windows
    /// `\Windows\System32` / `\Program Files\`;Unix `/etc /usr /bin /sbin /boot
    /// /dev /proc /sys`)仍拒。黑名单优先于白名单,防用户误授权系统目录。
    ///
    /// 输入 `candidate` 理想为 canonicalize 后的真实路径(防 symlink 逃逸);调用方
    /// (resolve_workspace_path_with_allowed)负责 canonicalize。但词法层预校验
    /// (`check_path_authorization`)**故意不 canonicalize**(路径可能不存在,如 write_file 新建),
    /// 会传入未归一的词法路径 → 与白名单(canonicalize 后)形态不对齐 → 已授权路径反复误弹窗。
    /// 故本函数内部 best-effort canonicalize 兜底(失败回退词法),消除所有调用方形态差异。
    /// 白名单仍是唯一真相源,canonicalize 不引入新放行,顺带解析 symlink 增强(非削弱)防逃逸。
    pub fn is_authorized(&self, candidate: &Path) -> bool {
        // Phase C: 黑名单优先(白名单命中也拒)。validate_path 已挡 .ssh/.aws 等,
        // 此处补系统核心目录(用户可能误把 C:\ 加入 persistent,黑名单兜底拒 System32)。
        if is_in_system_blacklist(candidate) {
            return false;
        }
        // F-260620 比对侧收口(误弹窗核心根因):白名单 persistent/session 是 canonicalize 后形态,
        // candidate 来自两类调用方形态不一(handler canonicalize 后 / 预校验词法未 canonicalize)。
        // 仅 strip_verbatim(去 \\?\ 前缀)不够——盘符大小写/.. /symlink/分隔符差异仍让 starts_with
        // 失败。best_effort_canonicalize 把 candidate 归一到真实路径(失败回退词法,不阻断合法访问),
        // 再 strip_verbatim 去前缀,使比对双侧形态完全对齐。e2ece2d 只收口 verbatim 前缀漏了这步。
        let cand = strip_verbatim(best_effort_canonicalize(candidate));
        self.persistent.iter().any(|d| cand.starts_with(d))
            || self.session.iter().any(|d| cand.starts_with(d))
            || self.once.iter().any(|d| cand.starts_with(d))
    }
}

/// F-260619-03 Phase C: 路径授权决策(预校验用,process_tool_calls 分类前调)。
///
/// `check_path_authorization` 返回本枚举,process_tool_calls 据此决定:
/// - `Authorized`:路径已授权 → 走原 Low/Med/High 流程
/// - `NeedsAuthorization`:路径未授权但非黑名单 → Phase B 挂起弹窗(emit AiDirAuthRequired)
/// - `Denied`:路径命中黑名单 → 硬拒(工具返 Err tool_result,不挂起)
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PathAuthDecision {
    /// 已授权(workspace_root / persistent / session 命中且非黑名单)
    Authorized,
    /// 未授权且非黑名单:需 Phase B 弹窗询问用户。附带规范化后的待授权目录(父目录,
    /// 对齐 session_trust 目录粒度),供"仅本次/未来都允许"写入白名单。
    NeedsAuthorization { dir: PathBuf },
    /// 命中系统黑名单(Phase C):硬拒,工具返 Err,不弹窗。
    Denied { reason: String },
}

/// F-260619-03 Phase C: 系统敏感目录黑名单判定(跨平台)。
///
/// 白名单命中但黑名单命中 → 拒(防用户误把整个盘符如 `C:\` 加入 persistent 致
/// System32 被放行)。validate_path(tool_registry.rs)已挡 .ssh/.aws/AppData 等,
/// 此处补系统核心目录。
///
/// - Windows(不区分大小写,按路径分隔符分段):`\Windows\System32`、`\Program Files\`、
///   `\Program Files (x86)\`、`\Windows\` 根下 System/SysWOW64
/// - Unix:`/etc`、`/usr`、`/bin`、`/sbin`、`/boot`、`/dev`、`/proc`、`/sys`
pub fn is_in_system_blacklist(path: &Path) -> bool {
    let s = path.to_string_lossy().to_lowercase();
    let seps = ['/', '\\'];
    // Windows:按分隔符分段判定(避免 contains 误伤 "my program files backup" 这类目录名)
    let segs: Vec<&str> = s.split(seps).filter(|s| !s.is_empty()).collect();
    for (i, seg) in segs.iter().enumerate() {
        // Windows 系统目录:C:\Windows(根本身及所有子目录 win.ini/explorer.exe/hosts 等)/
        // C:\Program Files / C:\Program Files (x86) / C:\ProgramData
        if cfg!(windows) {
            // windows 段命中即拒(含根本身,不再限定 system32 子目录 — 防 win.ini/hosts 等)
            if *seg == "windows" {
                return true;
            }
            // C:\Program Files / C:\Program Files (x86)
            if *seg == "program files" || *seg == "program files (x86)" {
                return true;
            }
            // C:\ProgramData(系统级应用数据)
            if *seg == "programdata" {
                return true;
            }
        }
        // 用户级凭据目录(.ssh/.aws/.gnupg):任意路径段命中即拒(跨平台,从 validate_path 迁移统一,
        // 消除 validate_path contains 与 is_in_system_blacklist 分段两套黑名单不一致)
        if matches!(*seg, ".ssh" | ".aws" | ".gnupg") {
            return true;
        }
        // Unix 系统目录(路径首段为这些即拒;Windows 上也防 Unix 风格绝对路径,防御性)
        if i == 0 && matches!(*seg, "etc" | "usr" | "bin" | "sbin" | "boot" | "dev" | "proc" | "sys") {
            return true;
        }
    }
    false
}

/// F-260619-03 Phase B/C: 路径授权预校验(供 process_tool_calls 分类前调)。
///
/// 词法层判定(不 canonicalize,因路径可能不存在 — write_file 新建)。返回三态决策:
/// - 路径规范化(去 .. / 锚定首个持久授权目录)后,若命中黑名单 → `Denied`
/// - 否则若 `is_authorized(规范化路径)`(persistent + session) → `Authorized`
/// - 否则 → `NeedsAuthorization { dir: 父目录规范化 }`(目录粒度,对齐 session_trust)
///
/// 注意:本函数只做词法层预判(防不存在路径兜底);实际执行时 handler 内
/// resolve_workspace_path_with_allowed 仍做完整校验(canonicalize symlink 防逃逸)。
/// 黑名单在两处都判(is_authorized 内 + 此处独立判),双保险。
pub fn check_path_authorization(
    raw_path: &str,
    allowed: &AllowedDirs,
) -> PathAuthDecision {
    // Phase C: 黑名单优先独立判定(is_authorized 内也判,此处先判便于 NeedsAuthorization
    // 不误把黑名单路径推到弹窗 — 黑名单路径直接硬拒不让用户"授权")。
    //
    // BUG-2026-07-07: 黑名单检查必须在路径规范化(is_absolute / root.join)之前对**原始路径**
    // 也判一次。原因:Windows 上 "/home/user/.ssh/config" 这类 Unix 风格路径 is_absolute() 返 false
    // (Windows 绝对路径需盘符或 UNC),会被当相对路径走 first_persistent_dir 分支;若 persistent 为空,
    // 旧逻辑直接 return NeedsAuthorization,**跳过黑名单检查** → .ssh/.aws 等敏感路径在 Windows 上绕过黑名单。
    // 修复:先用原始路径判黑名单(segment 匹配,任意路径段命中 .ssh 即拒,与路径形态无关),命中直接 Denied。
    let raw_resolved = Path::new(raw_path);
    if is_in_system_blacklist(raw_resolved) {
        return PathAuthDecision::Denied {
            reason: format!("路径命中系统敏感目录黑名单: {}", raw_path),
        };
    }
    // 规范化:绝对路径原样,相对路径锚定首个持久授权目录(与 resolve_workspace_path_impl 一致)
    let resolved = if raw_resolved.is_absolute() {
        PathBuf::from(raw_path)
    } else if let Some(root) = allowed.first_persistent_dir() {
        root.join(raw_path)
    } else {
        // 无授权目录时直接返 NeedsAuthorization(让引导流程处理),避免锚定到编译机路径。
        return PathAuthDecision::NeedsAuthorization {
            dir: PathBuf::from("."),
        };
    };
    // 规范化后再判一次黑名单(锚定后路径形态可能变化,双保险)
    if is_in_system_blacklist(&resolved) {
        return PathAuthDecision::Denied {
            reason: format!("路径命中系统敏感目录黑名单: {}", raw_path),
        };
    }
    if allowed.is_authorized(&resolved) {
        return PathAuthDecision::Authorized;
    }
    // 未授权 → 取父目录作待授权目录(目录粒度,对齐 session_trust 的 dir 语义)
    let dir = resolved
        .parent()
        .map(|p| p.to_path_buf())
        .unwrap_or_else(|| PathBuf::from("."));
    PathAuthDecision::NeedsAuthorization { dir }
}

/// Windows canonicalize 返回 `\\?\E:\...` 扩展路径(verbatim 前缀),与词法层(不 canonicalize)
/// 的 starts_with 比对不一致 → 白名单含但工具路径不匹配 → 误弹窗。strip 前缀统一为 `E:\...`。
pub fn strip_verbatim(p: PathBuf) -> PathBuf {
    let s = p.to_string_lossy().to_string();
    // Windows verbatim/设备路径前缀:\\?\ (Volume canonicalize)、\\.\ (设备命名空间)、
    // \??\ (对象管理器命名空间)。统一 strip 后与词法层 starts_with 比对一致。
    for prefix in [r"\\?\", r"\\.\", r"\??\"] {
        if let Some(stripped) = s.strip_prefix(prefix) {
            return PathBuf::from(stripped);
        }
    }
    p
}

/// best-effort canonicalize:把任意形态路径(词法/正斜杠/含../大小写不一)归一到真实路径,
/// 供 `is_authorized` 与白名单(canonicalize 后)比对侧形态对齐(误弹窗根因修复)。
///
/// - 路径存在 → `canonicalize` 成功,返回真实路径(大小写归一/.. 解析/symlink 解析/去冗余分隔符)
/// - 路径不存在(write_file 新建文件)→ canonicalize 其**父目录**(通常存在)+ 拼回文件名,
///   父目录也不存在 → 回退原词法路径(交由 strip_verbatim + starts_with 尽力匹配,不阻断)
///
/// 失败一律回退,绝不返回 Err——授权比对宁可降级匹配不可 panic/阻断合法访问。
pub(crate) fn best_effort_canonicalize(p: &Path) -> PathBuf {
    if let Ok(real) = std::fs::canonicalize(p) {
        return real;
    }
    if let Some(parent) = p.parent() {
        if let Ok(real_parent) = std::fs::canonicalize(parent) {
            if let Some(file_name) = p.file_name() {
                return real_parent.join(file_name);
            }
            return real_parent;
        }
    }
    p.to_path_buf()
}

/// F-260619-03 Phase B: 路径字符串等价比较(canonicalize 后比对,失败回退小写比对)。
pub fn paths_eq(a: &str, b: &str) -> bool {
    let pa = std::fs::canonicalize(a).map(|p| p.to_string_lossy().to_string()).unwrap_or_else(|_| a.to_string());
    let pb = std::fs::canonicalize(b).map(|p| p.to_string_lossy().to_string()).unwrap_or_else(|_| b.to_string());
    pa.eq_ignore_ascii_case(&pb)
}

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

    // ============================================================
    // F-260619-03 Phase A: AllowedDirs 授权语义测试
    // 锁定:persistent 命中放行 + 未命中拒绝。
    // 注:workspace_root 不再自动塞白名单(分发适配方案①),
    // 下方测试用显式 set.insert 模拟用户手动授权场景。
    // ============================================================

    /// workspace 根目录本地副本(原 `state.rs::workspace_root_path` 留在原文件
    /// 供旧 `.trash` 迁移使用;本模块测试需独立定义避免跨文件耦合)。
    fn workspace_root_path() -> PathBuf {
        PathBuf::from(env!("CARGO_MANIFEST_DIR"))
            .parent()
            .and_then(|p| p.parent())
            .map(PathBuf::from)
            .unwrap_or_else(|| PathBuf::from("."))
    }

    /// 显式授权目录后命中(模拟用户在 Settings 页添加目录)
    #[test]
    fn test_allowed_dirs_workspace_root_authorized() {
        let mut set = HashSet::new();
        set.insert(workspace_root_path());
        let allowed = AllowedDirs { persistent: set, session: HashSet::new(), once: HashSet::new() };
        let root = workspace_root_path();
        assert!(allowed.is_authorized(&root), "显式授权的 workspace_root 应命中");
        // workspace 内子路径也应授权(starts_with workspace_root)
        let child = root.join("src").join("main.rs");
        assert!(allowed.is_authorized(&child), "workspace_root 子路径应被授权");
    }

    /// 自定义授权目录命中放行(模拟用户授权 E:/some/external/dir)
    #[test]
    fn test_allowed_dirs_custom_authorized() {
        let mut set = HashSet::new();
        let custom = PathBuf::from("E:/wk-test-external-dir");
        set.insert(custom.clone());
        let allowed = AllowedDirs { persistent: set, session: HashSet::new(), once: HashSet::new() };
        // 精确命中 + 子路径 starts_with 命中
        assert!(allowed.is_authorized(&custom));
        assert!(allowed.is_authorized(&custom.join("sub").join("file.txt")));
    }

    /// 未授权目录被拒绝
    #[test]
    fn test_allowed_dirs_unauthorized_rejected() {
        let mut set = HashSet::new();
        set.insert(PathBuf::from("E:/wk-test-authorized"));
        let allowed = AllowedDirs { persistent: set, session: HashSet::new(), once: HashSet::new() };
        let outside = PathBuf::from("E:/wk-test-unauthorized/file.txt");
        assert!(!allowed.is_authorized(&outside), "未授权目录应被拒绝");
    }

    /// 默认(空 persistent)只授权 workspace_root
    #[test]
    fn test_allowed_dirs_default_empty_persistent() {
        let allowed = AllowedDirs::default();
        // 空 persistent,无 workspace_root → 任何路径都不授权
        // (default_with_root 才含 workspace_root;Default 不含,用于边界测试)
        assert!(!allowed.is_authorized(&PathBuf::from("E:/anything")));
    }

    /// SETTINGS_KEY 常量稳定(防 rename 致持久化数据丢失)
    #[test]
    fn test_allowed_dirs_settings_key_stable() {
        assert_eq!(AllowedDirs::SETTINGS_KEY, "allowed_dirs");
    }

    // ============================================================
    // F-260619-03 Phase B: session 临时授权语义测试
    // ============================================================

    /// Phase B: session 命中放行(persistent 未命中但 session 命中)
    #[test]
    fn test_allowed_dirs_session_authorized() {
        let mut session = HashSet::new();
        session.insert(PathBuf::from("E:/wk-temp-session"));
        let allowed = AllowedDirs { persistent: HashSet::new(), session, once: HashSet::new() };
        assert!(allowed.is_authorized(&PathBuf::from("E:/wk-temp-session/file.txt")));
    }

    /// Phase B: persistent + session 任一命中即放行
    #[test]
    fn test_allowed_dirs_persistent_or_session() {
        let mut persistent = HashSet::new();
        persistent.insert(PathBuf::from("E:/wk-persist"));
        let mut session = HashSet::new();
        session.insert(PathBuf::from("E:/wk-session"));
        let allowed = AllowedDirs { persistent, session, once: HashSet::new() };
        assert!(allowed.is_authorized(&PathBuf::from("E:/wk-persist/a")));
        assert!(allowed.is_authorized(&PathBuf::from("E:/wk-session/b")));
        assert!(!allowed.is_authorized(&PathBuf::from("E:/wk-other/c")));
    }

    // ============================================================
    // F-260619-03 Phase C: 系统目录黑名单测试
    // ============================================================

    /// Phase C: Windows System32 黑名单命中(即使在白名单内也拒)
    #[test]
    fn test_blacklist_windows_system32() {
        let mut persistent = HashSet::new();
        // 用户误把整个 C:\ 加入白名单
        persistent.insert(PathBuf::from("C:\\"));
        let allowed = AllowedDirs { persistent, session: HashSet::new(), once: HashSet::new() };
        // System32 应被黑名单拒(尽管 C:\ 在白名单)
        assert!(!allowed.is_authorized(&PathBuf::from("C:\\Windows\\System32\\config\\sam")));
    }

    /// Phase C: Windows Program Files 黑名单命中
    #[test]
    fn test_blacklist_windows_program_files() {
        let mut persistent = HashSet::new();
        persistent.insert(PathBuf::from("C:\\"));
        let allowed = AllowedDirs { persistent, session: HashSet::new(), once: HashSet::new() };
        assert!(!allowed.is_authorized(&PathBuf::from("C:\\Program Files\\SomeApp\\app.exe")));
    }

    /// Phase C: 黑名单不误伤合法目录(含 "program files" 子串的自定义目录名)
    #[test]
    fn test_blacklist_no_false_positive() {
        // "my program files backup" 不应被拒(分段匹配,非精确段)
        assert!(!is_in_system_blacklist(&PathBuf::from("E:/my program files backup/x")));
        // 普通工作目录不拒
        assert!(!is_in_system_blacklist(&PathBuf::from("E:/wk-lab/devflow/src")));
    }

    /// Phase C: Unix 系统目录黑名单(/etc /usr /bin 等)
    #[test]
    fn test_blacklist_unix_system_dirs() {
        assert!(is_in_system_blacklist(&PathBuf::from("/etc/passwd")));
        assert!(is_in_system_blacklist(&PathBuf::from("/usr/bin/python")));
        assert!(is_in_system_blacklist(&PathBuf::from("/proc/self/environ")));
        assert!(is_in_system_blacklist(&PathBuf::from("/sys/kernel")));
        // 普通用户目录不拒
        assert!(!is_in_system_blacklist(&PathBuf::from("/home/user/project")));
    }

    // ============================================================
    // F-260619-03 Phase B/C: check_path_authorization 三态决策测试
    // ============================================================

    /// Phase B/C: 显式授权 workspace_root 后,内路径 → Authorized(方案①:不再默认)
    #[test]
    fn test_check_path_authorized_workspace() {
        let mut set = HashSet::new();
        set.insert(workspace_root_path());
        let allowed = AllowedDirs { persistent: set, session: HashSet::new(), once: HashSet::new() };
        let rel = "src/main.rs";
        match check_path_authorization(rel, &allowed) {
            PathAuthDecision::Authorized => {}
            other => panic!("workspace_root 内路径应 Authorized, got {:?}", other),
        }
    }

    /// Phase B/C: 未授权路径 → NeedsAuthorization(附父目录)
    #[test]
    fn test_check_path_needs_auth() {
        let allowed = AllowedDirs::default(); // 空,无 workspace_root
        match check_path_authorization("E:/wk-external/file.txt", &allowed) {
            PathAuthDecision::NeedsAuthorization { dir } => {
                assert_eq!(dir, PathBuf::from("E:/wk-external"));
            }
            other => panic!("未授权路径应 NeedsAuthorization, got {:?}", other),
        }
    }

    /// Phase B: session 命中 → Authorized(check_path_authorization 读 session 字段)
    #[test]
    fn test_check_path_session_hit() {
        let mut session = HashSet::new();
        session.insert(PathBuf::from("E:/wk-session"));
        let allowed = AllowedDirs { persistent: HashSet::new(), session, once: HashSet::new() };
        match check_path_authorization("E:/wk-session/sub/file.txt", &allowed) {
            PathAuthDecision::Authorized => {}
            other => panic!("session 命中应 Authorized, got {:?}", other),
        }
    }

    /// Phase C: 黑名单路径 → Denied(不弹窗直接拒)
    #[test]
    fn test_check_path_denied_blacklist() {
        let allowed = AllowedDirs::default_with_root();
        match check_path_authorization("C:/Windows/System32/config/sam", &allowed) {
            PathAuthDecision::Denied { .. } => {}
            other => panic!("System32 应 Denied, got {:?}", other),
        }
    }

    /// BUG-2026-07-07: Windows 上 Unix 风格 .ssh 路径必须被黑名单拒(不能绕过)。
    ///
    /// 背景:Windows 上 `/home/user/.ssh/config` 的 is_absolute() 返 false(无盘符),
    /// 旧逻辑在 persistent 为空时直接返 NeedsAuthorization,**跳过黑名单检查**,
    /// 致 .ssh/.aws 等敏感路径在 Windows 上绕过黑名单。修复后原始路径也判黑名单。
    /// 此测试锁死该修复:空 persistent + Unix 风格 .ssh 路径 → 必 Denied。
    #[test]
    fn test_check_path_blacklist_unix_style_ssh_on_windows() {
        let allowed = AllowedDirs::default_with_root();
        match check_path_authorization("/home/user/.ssh/config", &allowed) {
            PathAuthDecision::Denied { .. } => {}
            other => panic!("Unix 风格 .ssh 路径应 Denied(黑名单), got {:?}", other),
        }
        // 同理 .aws / .gnupg
        match check_path_authorization("/home/user/.aws/credentials", &allowed) {
            PathAuthDecision::Denied { .. } => {}
            other => panic!("Unix 风格 .aws 路径应 Denied(黑名单), got {:?}", other),
        }
    }

    /// 回归:合法相对路径(非黑名单)在空 persistent 下仍应返 NeedsAuthorization(引导授权)。
    /// 修复黑名单提前后,需确认非黑名单相对路径行为不变。
    #[test]
    fn test_check_path_non_blacklist_relative_still_needs_auth() {
        let allowed = AllowedDirs::default_with_root();
        match check_path_authorization("some/regular/path", &allowed) {
            PathAuthDecision::NeedsAuthorization { .. } => {}
            other => panic!("非黑名单相对路径 + 空 persistent 应 NeedsAuthorization, got {:?}", other),
        }
    }


    // ============================================================
    // F-260620: strip_verbatim 比对侧收口 + 黑名单增强测试
    // 三方审查(安全/UX/跨端)交叉印证:strip_verbatim 仅写入侧调用,比对侧遗漏致误弹窗。
    // ============================================================

    /// F-260620: is_authorized 比对侧 strip_verbatim — candidate 带 \\?\ 前缀也命中白名单
    /// (handler canonicalize 后路径 vs persistent 词法路径,形态不一致曾致误弹窗)。
    /// persistent 用动态 workspace_root_path(非硬编码开发机路径),candidate 拼 verbatim 前缀。
    #[test]
    fn test_is_authorized_strips_verbatim_prefix() {
        let mut set = HashSet::new();
        set.insert(workspace_root_path());
        let allowed = AllowedDirs { persistent: set, session: HashSet::new(), once: HashSet::new() };
        let child = workspace_root_path().join("src").join("main.rs");
        // candidate 带 verbatim 前缀(handler canonicalize 后形态)应命中白名单
        let verbatim = PathBuf::from(format!("{}{}", r"\\?\", child.to_string_lossy()));
        assert!(allowed.is_authorized(&verbatim), "verbatim 前缀路径应命中白名单");
        // 设备命名空间前缀(\\.\)
        let dev = PathBuf::from(format!("{}{}", r"\\.\", child.to_string_lossy()));
        assert!(allowed.is_authorized(&dev), "设备命名空间前缀路径应命中白名单");
    }

    /// F-260620: strip_verbatim 三前缀(\\?\ / \\.\ / \??\)统一去除
    #[test]
    fn test_strip_verbatim_three_prefixes() {
        assert_eq!(strip_verbatim(PathBuf::from(r"\\?\E:\foo")), PathBuf::from("E:\\foo"));
        assert_eq!(strip_verbatim(PathBuf::from(r"\\.\E:\foo")), PathBuf::from("E:\\foo"));
        assert_eq!(strip_verbatim(PathBuf::from(r"\??\E:\foo")), PathBuf::from("E:\\foo"));
        // 无前缀原样返回
        assert_eq!(strip_verbatim(PathBuf::from("E:\\foo")), PathBuf::from("E:\\foo"));
    }

    /// F-260620: 黑名单增强 — Windows 根本身 / ProgramData / 用户凭据目录(.ssh/.aws/.gnupg)
    #[test]
    fn test_blacklist_windows_root_programdata_creds() {
        // C:\Windows 根本身(不再限定 system32 子目录 — 防 win.ini/hosts 等)
        assert!(is_in_system_blacklist(&PathBuf::from("C:\\Windows\\win.ini")));
        assert!(is_in_system_blacklist(&PathBuf::from("C:\\Windows\\explorer.exe")));
        // ProgramData
        assert!(is_in_system_blacklist(&PathBuf::from("C:\\ProgramData\\App\\config")));
        // 用户凭据目录(从 validate_path 迁移统一,任意路径段命中即拒)
        assert!(is_in_system_blacklist(&PathBuf::from("C:\\Users\\user\\.ssh\\id_rsa")));
        assert!(is_in_system_blacklist(&PathBuf::from("/home/user/.aws/credentials")));
        assert!(is_in_system_blacklist(&PathBuf::from("/home/user/.gnupg/pubring.gpg")));
        // 普通目录不误伤(devflow 自身 / 含 appdata 段的合法备份目录)
        assert!(!is_in_system_blacklist(&PathBuf::from("E:\\wk-lab\\devflow\\src")));
        assert!(!is_in_system_blacklist(&PathBuf::from("D:\\backup\\appdata\\data")));
    }
}
