//! 声明式工具注册基础设施(tool_registry 拆分第一步)
//!
//! 背景:`tool_registry.rs` 4224 行,每加一个工具手写一段
//! `registry.register(name, desc, object_schema(...), RiskLevel::X, { ... Box::new ... })`,
//! 5 段重复样板 + 闭包捕获 + 缩进极易出错。本模块提供声明式宏 `declare_tool!`,
//! 把工具定义收敛为「名字 / 描述 / schema / 风险 / handler 块」五要素一行式声明。
//!
//! 设计取舍(三选一权衡):
//! - A. proc-macro `#[ai_tool]` 属性宏:需新建 proc-macro crate + syn/quote 重依赖,违反
//!   「不引新依赖」。否决。
//! - B. `inventory` crate 自动收集:新外部依赖 + 拉入 linkme/ctor 运行期注册语义,
//!   与现有 `register_*(&mut registry)` 命令式收集并存需双重 source-of-truth。否决。
//! - C. **`macro_rules!` 声明式宏(本方案)**:零新依赖(纯 std `macro_rules!`),
//!   展开为等价的 `registry.register(...)` 调用——与现有 48 个 `register` 调用**完全并存**,
//!   旧工具零改动,新工具可选声明式。handler 块就地书写,`$db`/`$registry` 等捕获变量
//!   原样透传,语义 1:1 等价(schema/risk/handler 同源不变)。
//!
//! 试点:`list_projects` 已迁至声明式(见 commands/ai/tools/list_projects.rs),验证编译过 +
//! 行为等价 + 48 工具基线不破。其余 47 个工具后续批次渐进迁移,不在此步。
//!
//! 宏展开示例(输入):
//! ```ignore
//! declare_tool!(registry, db: Arc<Database>, "list_projects",
//!     "列出所有项目...", RiskLevel::Low,
//!     schema: object_schema(vec![("offset", "integer", false), ("limit", "integer", false)]),
//!     args => {
//!         let repo = ProjectRepo::new(&db);
//!         let items = repo.list_active().await?;
//!         // ...
//!         Ok(json!({ "items": items, ... }))
//!     });
//! ```
//! 展开后等价于现有手写的:
//! ```ignore
//! registry.register(
//!     "list_projects", "...",
//!     object_schema(vec![...]), RiskLevel::Low,
//!     { let db = db.clone(); Box::new(move |args: serde_json::Value| {
//!         let db = db.clone();
//!         Box::pin(async move { /* handler body */ })
//!     })},
//! );
//! ```

/// 声明式注册一个 AI 工具到 `$registry`。
///
/// 与手写 `registry.register(name, desc, schema, risk, handler)` 语义 1:1 等价,
/// 仅消除闭包包装样板(`{ let db = db.clone(); Box::new(move |args| { let db = db.clone(); Box::pin(async move { ... }) }) }`)。
///
/// 形参:
/// - `$registry`: `&mut AiToolRegistry` 注册表句柄。
/// - `$capture: $cap_ty`: handler 需捕获的外部变量(如 `db: Arc<Database>`)。宏自动 clone 进闭包,
///   handler body 内以 `$capture` 名访问。无捕获工具用 `_` 占位(并保证 body 不引用它)。
/// - `$name`: 工具名 `&str`。
/// - `$desc`: 工具描述 `&str`(发给 LLM)。
/// - `$risk`: `RiskLevel`(如 `RiskLevel::Low`)。
/// - `$schema`: 参数 JSON Schema(`serde_json::Value`),常用 `object_schema(...)`。
/// - `$handler`: 一个 **block 表达式**(花括号体),返回 `anyhow::Result<serde_json::Value>`。
///   体内在 `$args`(serde_json::Value)与 `$capture`(克隆后的捕获变量)上工作。
///   宏负责把它包进 `async move { ... }` 并 `Box::pin`——故调用方只写「同步语义的体」,
///   不写 `async move`/`Box::pin`/`Box::new` 三层样板。
///
/// 注意:此宏不替代 `register`,而是包装它——`register` 仍是 `AiToolRegistry` 的唯一注册入口,
/// 宏仅是语法糖。现有 48 个手写 `register` 调用不动,新工具改用 `declare_tool!`。
#[macro_export]
macro_rules! declare_tool {
    (
        $registry:expr,
        $capture:ident : $cap_ty:ty,
        $name:expr,
        $desc:expr,
        $risk:expr,
        schema: $schema:expr,
        $args:ident => $handler:block
    ) => {{
        let __cap: $cap_ty = $capture.clone();
        $registry.register(
            $name, $desc, $schema, $risk,
            {
                let $capture = __cap.clone();
                ::std::boxed::Box::new(move |$args: ::serde_json::Value| {
                    let $capture = $capture.clone();
                    ::std::boxed::Box::pin(async move {
                        let $capture: $cap_ty = $capture;
                        $handler
                    })
                        as ::std::pin::Pin<::std::boxed::Box<
                            dyn ::std::future::Future<Output = ::anyhow::Result<::serde_json::Value>>
                                + ::std::marker::Send,
                        >>
                })
            },
        );
    }};
}

#[cfg(test)]
mod tests {
    use crate::ai_tools::{object_schema, AiToolRegistry, RiskLevel};

    /// 声明式宏注册的工具与手写 register 行为等价(name/schema/risk/执行结果一致)。
    /// 这是「现有工具零回归 + 语义等价」的最小验证(试点 list_projects 迁移的微观镜像)。
    #[tokio::test]
    async fn declare_tool_equivalent_to_register() {
        let mut reg = AiToolRegistry::new();

        // 声明式注册(新基础设施)
        let counter: std::sync::Arc<std::sync::atomic::AtomicUsize> =
            std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
        declare_tool!(
            reg,
            counter: std::sync::Arc<std::sync::atomic::AtomicUsize>,
            "echo_decl",
            "声明式 echo 工具",
            RiskLevel::Low,
            schema: object_schema(vec![("msg", "string", true)]),
            args => {
                counter.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
                let msg = args["msg"].as_str().unwrap_or("");
                Ok(serde_json::json!({ "echo": msg }))
            }
        );

        // 等价断言:定义层(name 存在 / risk 正确 / schema 透传)
        assert_eq!(reg.len(), 1, "声明式宏应注册 1 个工具");
        let tool = reg.get("echo_decl").expect("echo_decl 应已注册");
        assert_eq!(tool.risk_level, RiskLevel::Low);
        assert_eq!(tool.definition.function.name, "echo_decl");
        assert_eq!(tool.definition.function.parameters["properties"]["msg"]["type"], "string");

        // 执行等价:handler 收到 args、捕获变量可访问、返回 handler body 的 Value
        let out = reg.execute("echo_decl", serde_json::json!({ "msg": "hi" })).await.unwrap();
        assert_eq!(out["echo"], "hi");
        assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1, "捕获变量应在执行时递增");
    }

    /// 无捕获工具(纯计算/常量返回)用占位 capture,handler 不引用它。
    #[tokio::test]
    async fn declare_tool_no_capture() {
        let mut reg = AiToolRegistry::new();
        let dummy: std::sync::Arc<()> = std::sync::Arc::new(()); // 无真实捕获,占位
        declare_tool!(
            reg,
            dummy: std::sync::Arc<()>,
            "const_tool",
            "无捕获常量工具",
            RiskLevel::Medium,
            schema: object_schema(vec![]),
            _args => { Ok(serde_json::json!({ "ok": true })) }
        );
        let out = reg.execute("const_tool", serde_json::json!({})).await.unwrap();
        assert_eq!(out["ok"], true);
    }
}
