use serde_json::{json, Value};

use crate::commands::describe::{self, ArgDescriptor, CommandDescriptor};
use crate::extensions;
use crate::mcp::protocol::Tool;

/// (mcp_command_name, full_cli_command_path)
pub(crate) type CommandPath = (&'static str, &'static [&'static str]);

pub(crate) struct ToolDef {
    pub(crate) name: &'static str,
    pub(crate) description: &'static str,
    pub(crate) commands: &'static [CommandPath],
    /// Arg names to rename in the MCP schema: (from, to).
    pub(crate) renames: &'static [(&'static str, &'static str)],
    /// CLI arg names to omit from the auto-generated MCP schema.
    pub(crate) exclude: &'static [&'static str],
    /// Extra properties to inject into the MCP schema after auto-generation: (name, JSON schema).
    pub(crate) extra_props: &'static [(&'static str, &'static str)],
}

const RUNTIME_REQUEST_PROP: (&str, &str) = (
    "runtime_request",
    r#"{"type":"object","description":"Optional shared runtime envelope metadata echoed in machine responses.","properties":{"api_family":{"type":"string","enum":["ccd-runtime"]},"api_version":{"type":"integer","enum":[1]},"request_id":{"type":"string"},"actor_id":{"type":"string"},"supervisor_id":{"type":"string"},"host_session_id":{"type":"string"},"host_run_id":{"type":"string"},"host_task_id":{"type":"string"},"approval_request_id":{"type":"string"},"idempotency_key":{"type":"string"}},"required":["api_family","api_version","request_id"]}"#,
);

const APPEND_PROTECTED_WRITE_PROP: (&str, &str) = (
    "protected_write",
    r#"{"type":"object","description":"Protected-write envelope for append-oriented autonomous surfaces. When the workspace is under an active autonomous lease, provide actor_id and session_id matching the active owner (or explicitly authorized supervisor for clear-by-id). expected_revision is optional and ignored for append semantics.","properties":{"actor_id":{"type":"string"},"session_id":{"type":"string"},"expected_revision":{"type":"integer","minimum":0}}}"#,
);

const CORE_TOOLS: &[ToolDef] = &[
    ToolDef {
        name: "ccd_repo",
        description: "Repo lifecycle: attach scaffold link unlink gc skills-install",
        commands: &[
            ("attach", &["attach"]),
            ("scaffold", &["scaffold"]),
            ("link", &["link"]),
            ("unlink", &["unlink"]),
            ("gc", &["gc"]),
            ("skills-install", &["skills", "install"]),
        ],
        renames: &[],
        exclude: &[],
        extra_props: &[],
    },
    ToolDef {
        name: "ccd_health",
        description: "Validate: doctor check drift sync preflight hooks",
        commands: &[
            ("check", &["check"]),
            ("preflight", &["preflight"]),
            ("doctor", &["doctor"]),
            ("drift", &["drift"]),
            ("sync", &["sync"]),
            ("hooks-install", &["hooks", "install"]),
            ("hooks-check", &["hooks", "check"]),
        ],
        renames: &[],
        exclude: &[],
        extra_props: &[],
    },
    ToolDef {
        name: "ccd_session",
        description:
            "Startup context and shared-checkout helpers: start open state-start state-clear",
        commands: &[
            ("start", &["start"]),
            ("session-open", &["session", "open"]),
            ("session-state-start", &["session-state", "start"]),
            ("session-state-clear", &["session-state", "clear"]),
        ],
        renames: &[("from_ref", "from")],
        // The MCP surface stays compact and capability-oriented: `fields` is
        // replaced by `full`, `check` is a CLI exit-code mode, and wrapper-
        // first startup stays on the host-hook CLI surface rather than as an
        // MCP macro.
        exclude: &[
            "fields",
            "memory_depth",
            "check",
            "lifecycle",
            "owner_kind",
            "actor_id",
            "supervisor_id",
            "lease_seconds",
            "reason",
            "activate",
        ],
        extra_props: &[],
    },
    ToolDef {
        name: "ccd_session_lifecycle",
        description: "Runtime session lifecycle: start heartbeat clear takeover",
        commands: &[
            ("start-session", &["session-state", "start"]),
            ("heartbeat-session", &["session-state", "heartbeat"]),
            ("clear-session", &["session-state", "clear"]),
            ("takeover-session", &["session-state", "takeover"]),
        ],
        renames: &[],
        exclude: &[
            "lifecycle",
            "owner_kind",
            "actor_id",
            "supervisor_id",
            "lease_seconds",
            "reason",
            "activity",
            "full",
        ],
        extra_props: &[
            (
                "session_owner",
                r#"{"type":"object","description":"Session ownership and lifecycle options. `start-session` defaults to interactive when omitted. Autonomous lifecycle flows require actor_id and lease_seconds, except `recover_stale` start-session preserves the stale owner's lease_seconds when omitted. `owner_kind` defaults to `runtime_worker` when omitted and may be set to `runtime_supervisor` for an explicit supervisor-owned session path. `recover_stale` requires the same actor as the stale owner; `takeover-session` requires a different actor_id and may carry supervisor_id.","properties":{"lifecycle":{"type":"string","enum":["interactive","autonomous"]},"owner_kind":{"type":"string","enum":["runtime_worker","runtime_supervisor"]},"actor_id":{"type":"string"},"supervisor_id":{"type":"string"},"lease_seconds":{"type":"integer","minimum":1},"recover_stale":{"type":"boolean"}}}"#,
            ),
            RUNTIME_REQUEST_PROP,
        ],
    },
    ToolDef {
        name: "ccd_session_gates",
        description: "Execution gates: list replace seed set-status advance clear",
        commands: &[
            ("list-gates", &["session-state", "gates", "list"]),
            ("replace-gates", &["session-state", "gates", "replace"]),
            ("seed-gates", &["session-state", "gates", "seed"]),
            ("set-gate-status", &["session-state", "gates", "set-status"]),
            ("advance-gates", &["session-state", "gates", "advance"]),
            ("clear-gates", &["session-state", "gates", "clear"]),
        ],
        renames: &[],
        exclude: &[
            "actor_id",
            "session_id",
            "expected_revision",
            "gates",
            "full",
        ],
        extra_props: &[
            (
                "protected_write",
                r#"{"type":"object","description":"Protected-write envelope for autonomous owner surfaces. When the workspace is under an active autonomous lease, provide actor_id, session_id, and expected_revision matching the active owner.","properties":{"actor_id":{"type":"string"},"session_id":{"type":"string"},"expected_revision":{"type":"integer","minimum":0}}}"#,
            ),
            (
                "gates",
                r#"{"type":"array","description":"Execution gate texts (required for: replace-gates).","items":{"type":"string"}}"#,
            ),
            RUNTIME_REQUEST_PROP,
        ],
    },
    ToolDef {
        name: "ccd_escalation",
        description: "Escalation state: list set clear",
        commands: &[
            ("list-escalations", &["escalation-state", "list"]),
            ("set-escalation", &["escalation-state", "set"]),
            ("clear-escalation", &["escalation-state", "clear"]),
        ],
        renames: &[],
        exclude: &["full", "actor_id", "session_id", "expected_revision"],
        extra_props: &[APPEND_PROTECTED_WRITE_PROP, RUNTIME_REQUEST_PROP],
    },
    ToolDef {
        name: "ccd_recovery",
        description: "Recovery artifacts: write checkpoint or working buffer",
        commands: &[("write-recovery", &["recovery", "write"])],
        renames: &[],
        exclude: &["actor_id", "session_id", "expected_revision"],
        extra_props: &[
            (
                "origin",
                r#"{"type":"string","enum":["manual","compaction","risky_pause"]}"#,
            ),
            (
                "checkpoint",
                r#"{"type":"object","description":"Checkpoint payload to persist.","properties":{"summary":{"type":"string"},"immediate_actions":{"type":"array","items":{"type":"string"}},"key_files":{"type":"array","items":{"type":"string"}}},"required":["summary"]}"#,
            ),
            (
                "working_buffer",
                r#"{"type":"object","description":"Working-buffer payload to persist.","properties":{"summary_lines":{"type":"array","items":{"type":"string"}}},"required":["summary_lines"]}"#,
            ),
            APPEND_PROTECTED_WRITE_PROP,
            RUNTIME_REQUEST_PROP,
        ],
    },
    ToolDef {
        name: "ccd_state",
        description: "State and governance: export radar checkpoint handoff policy escalation",
        commands: &[
            ("runtime-state-export", &["runtime-state", "export"]),
            ("handover", &["handover"]),
            ("checkpoint", &["checkpoint"]),
            ("handoff-refresh", &["handoff", "refresh"]),
            ("policy-check", &["policy-check"]),
            ("escalation-state-list", &["escalation-state", "list"]),
            ("escalation-state-set", &["escalation-state", "set"]),
        ],
        renames: &[],
        exclude: &[
            "fields",
            "memory_depth",
            "actor_id",
            "session_id",
            "expected_revision",
            "id",
            "commit",
            "close_clean",
            "since_session",
        ],
        extra_props: &[],
    },
    ToolDef {
        name: "ccd_delegation",
        description: "Delegation bootstrap: bounded child context for runtime-driven sub-agents",
        commands: &[("child-bootstrap", &["runtime-state", "child-bootstrap"])],
        renames: &[],
        exclude: &[],
        extra_props: &[],
    },
    ToolDef {
        name: "ccd_context",
        description: "Mid-session refresh evaluation: context-check",
        commands: &[("context-check", &["context-check"])],
        renames: &[],
        exclude: &["fields", "memory_depth"],
        extra_props: &[RUNTIME_REQUEST_PROP],
    },
    ToolDef {
        name: "ccd_memory_capture",
        description: "Bounded evidence capture: evidence-submit candidate-extract",
        commands: &[
            ("memory-evidence-submit", &["memory", "evidence", "submit"]),
            (
                "memory-candidate-extract",
                &["memory", "candidate", "extract"],
            ),
        ],
        renames: &[],
        exclude: &[
            "scope",
            "entry_type",
            "source_kind",
            "source_ref",
            "host_hook",
            "host",
            "provider",
            "provider_ref",
            "summary",
            "evidence_id",
            "limit",
            "actor_id",
            "session_id",
            "expected_revision",
            "full",
        ],
        extra_props: &[
            (
                "evidence_envelope",
                r#"{"type":"object","description":"Bounded evidence summary payload (required for: memory-evidence-submit). Scope is canonical CCD scope: workspace, work-stream, project, profile, or project-truth. Optional host/provider references preserve attribution without storing raw transcript bodies.","properties":{"scope":{"type":"string","enum":["workspace","work-stream","project","profile","project-truth"]},"type":{"type":"string","enum":["rule","constraint","heuristic","observation","attempt"]},"source_kind":{"type":"string","enum":["transcript","session","event-stream","hook-output","log","document"]},"summary":{"type":"string"},"source_ref":{"type":"string"},"host":{"type":"string"},"host_hook":{"type":"string","enum":["on-session-start","before-prompt-build","on-compaction-notice","on-agent-end","on-session-end","supervisor-tick"]},"provider":{"type":"string"},"provider_ref":{"type":"string"}},"required":["scope","type","source_kind","summary"]}"#,
            ),
            (
                "extract_options",
                r#"{"type":"object","description":"Candidate extraction selection (optional for: memory-candidate-extract). Omit to scan pending evidence oldest-first.","properties":{"evidence_id":{"type":"string"},"limit":{"type":"integer","minimum":1}}}"#,
            ),
            APPEND_PROTECTED_WRITE_PROP,
            RUNTIME_REQUEST_PROP,
        ],
    },
    ToolDef {
        name: "ccd_memory",
        description: "Memory follow-through: candidate-admit compact promote",
        commands: &[
            ("memory-candidate-admit", &["memory", "candidate", "admit"]),
            ("memory-compact", &["memory", "compact"]),
            ("memory-promote", &["memory", "promote"]),
        ],
        renames: &[],
        // Compact and promote each need command-specific parameters. Keep both within the
        // 10-property MCP budget by collapsing command-specific options into objects.
        exclude: &[
            "destination",
            "source_scope",
            "target_file",
            "source_outcome",
            "scope",
            "entry_type",
            "source_kind",
            "source_ref",
            "host_hook",
            "host",
            "provider",
            "provider_ref",
            "summary",
            "evidence_id",
            "limit",
            "actor_id",
            "session_id",
            "expected_revision",
            "keep",
            "decay_class",
            "review",
            "remove",
            "full",
        ],
        extra_props: &[
            (
                "compact_options",
                r#"{"type":"object","description":"Compaction options (required for: memory-compact). Include scope: profile, project, work-stream, or workspace. Then use {\"keep\":\"<id>\"}, {\"decay_class\":\"stable\"}, or {\"review\":\"expired\"}; optional remove: true when reviewing a selected entry.","properties":{"scope":{"type":"string","enum":["profile","project","work-stream","workspace"]},"keep":{"type":"string"},"decay_class":{"type":"string","enum":["permanent","stable","active"]},"review":{"type":"string","enum":["expired","superseded","promotion-candidate"]},"remove":{"type":"boolean"}}}"#,
            ),
            (
                "candidate_route",
                r#"{"type":"object","description":"Candidate admission route (required for: memory-candidate-admit).","properties":{"source_scope":{"type":"string","enum":["clone-memory","branch-memory"]},"destination":{"type":"string","enum":["branch-memory","repo-memory"]}},"required":["source_scope","destination"]}"#,
            ),
            (
                "promote_destination",
                r#"{"type":"object","description":"Promotion options (required for: memory-promote). Use {\"type\":\"work-stream-memory\"} for workspace->work-stream promotion, {\"type\":\"project-memory\"} for work-stream/workspace->project promotion, {\"type\":\"profile-memory\"} for project->profile promotion, or {\"type\":\"project-truth\",\"target_file\":\"/path/to/file\"}; optional source_outcome: active, superseded, or link-only.","properties":{"type":{"type":"string","enum":["work-stream-memory","project-memory","profile-memory","project-truth"]},"target_file":{"type":"string"},"source_outcome":{"type":"string","enum":["active","superseded","link-only"]}},"required":["type"]}"#,
            ),
            APPEND_PROTECTED_WRITE_PROP,
            RUNTIME_REQUEST_PROP,
        ],
    },
    ToolDef {
        name: "ccd_memory_recall",
        description: "Memory recall and ingest reporting",
        commands: &[
            ("memory-search", &["memory", "search"]),
            ("memory-describe", &["memory", "describe"]),
            ("memory-expand", &["memory", "expand"]),
            ("memory-sync-status", &["memory", "sync-status"]),
            ("memory-source-map", &["memory", "source-map"]),
        ],
        renames: &[],
        exclude: &["full"],
        extra_props: &[RUNTIME_REQUEST_PROP],
    },
];

/// Build the available MCP tool definitions from the CLI describe schema.
pub fn build_tools() -> Vec<Tool> {
    let schema = describe::run();
    let mut tools = Vec::new();
    for def in CORE_TOOLS {
        if def.name == "ccd_memory" {
            tools.extend(extensions::build_mcp_tools(&schema.commands));
        }
        tools.push(build_tool(def, &schema.commands));
    }
    tools
}

pub(crate) fn build_tool(def: &ToolDef, commands: &[CommandDescriptor]) -> Tool {
    let mut command_names: Vec<&str> = Vec::new();
    // Track (prop_name, ArgDescriptor ref, set of mcp command names that carry this arg,
    //        set of mcp command names where this arg is required)
    let mut arg_info: Vec<(String, &ArgDescriptor, Vec<&str>, Vec<&str>)> = Vec::new();

    for &(mcp_name, path) in def.commands {
        command_names.push(mcp_name);

        let cmd_desc = find_command(commands, path)
            .unwrap_or_else(|| panic!("command not found in describe tree: {}", path.join(" ")));

        for arg in &cmd_desc.args {
            let prop_name = rename_arg(&arg.name, def.renames);
            if let Some(entry) = arg_info.iter_mut().find(|(n, _, _, _)| *n == prop_name) {
                entry.2.push(mcp_name);
                if arg.required {
                    entry.3.push(mcp_name);
                }
            } else {
                let required_by = if arg.required { vec![mcp_name] } else { vec![] };
                arg_info.push((prop_name, arg, vec![mcp_name], required_by));
            }
        }
    }

    let total_cmds = command_names.len();

    // Build inputSchema
    let mut props = serde_json::Map::new();
    props.insert(
        "command".to_owned(),
        json!({
            "type": "string",
            "enum": command_names,
            "description": "Command to execute"
        }),
    );

    if !def.exclude.contains(&"full") {
        props.insert(
            "full".to_owned(),
            json!({
                "type": "boolean",
                "default": false,
                "description": "Return the full report. Default false uses compact MCP output for verbose commands."
            }),
        );
    }

    let mut required: Vec<String> = vec!["command".to_owned()];

    for (name, arg, _present_in, required_by) in &arg_info {
        if def.exclude.contains(&name.as_str()) {
            continue;
        }
        let mut schema = arg_to_schema(arg);
        if !required_by.is_empty() {
            if required_by.len() == total_cmds {
                // Required by ALL commands — add to schema required array
                required.push(name.clone());
            } else {
                // Required by only some commands — annotate description
                let suffix = format!(" (required for: {})", required_by.join(", "));
                if let Some(obj) = schema.as_object_mut() {
                    let desc = obj
                        .get("description")
                        .and_then(|v| v.as_str())
                        .unwrap_or("")
                        .to_owned();
                    obj.insert("description".to_owned(), json!(format!("{desc}{suffix}")));
                }
            }
        }
        props.insert(name.clone(), schema);
    }

    for (name, schema_str) in def.extra_props {
        let schema: Value = serde_json::from_str(schema_str)
            .unwrap_or_else(|e| panic!("invalid extra_props schema for {}: {e}", def.name));
        props.insert((*name).to_owned(), schema);
    }

    let input_schema = json!({
        "type": "object",
        "properties": props,
        "required": required
    });

    // Budget assertions (enforced in debug builds)
    let prop_count = props.len();
    let cmd_count = command_names.len();
    debug_assert!(
        prop_count <= 10,
        "tool {} has {} properties (budget: 10)",
        def.name,
        prop_count
    );
    debug_assert!(
        cmd_count <= 7,
        "tool {} has {} command enum values (budget: 7)",
        def.name,
        cmd_count
    );
    debug_assert!(
        def.description.len() <= 80,
        "tool {} description is {} chars (budget: 80)",
        def.name,
        def.description.len()
    );

    Tool {
        name: def.name.to_owned(),
        description: def.description.to_owned(),
        input_schema,
    }
}

fn find_command<'a>(
    commands: &'a [CommandDescriptor],
    path: &[&str],
) -> Option<&'a CommandDescriptor> {
    let (head, tail) = path.split_first()?;
    let command = commands.iter().find(|c| c.name == *head)?;
    if tail.is_empty() {
        Some(command)
    } else {
        find_command(&command.subcommands, tail)
    }
}

fn rename_arg(name: &str, renames: &[(&str, &str)]) -> String {
    for &(from, to) in renames {
        if name == from {
            return to.to_owned();
        }
    }
    name.to_owned()
}

fn arg_to_schema(arg: &ArgDescriptor) -> Value {
    let is_bool = arg.possible_values.as_ref().is_some_and(|pv| {
        pv.len() == 2 && pv.contains(&"true".to_owned()) && pv.contains(&"false".to_owned())
    });

    let mut schema = serde_json::Map::new();

    if is_bool {
        schema.insert("type".to_owned(), json!("boolean"));
    } else {
        schema.insert("type".to_owned(), json!("string"));
        if let Some(ref pv) = arg.possible_values {
            schema.insert("enum".to_owned(), json!(pv));
        }
    }

    if let Some(ref help) = arg.help {
        let desc = truncate(help, 60);
        schema.insert("description".to_owned(), json!(desc));
    }

    if let Some(ref default) = arg.default {
        if is_bool {
            schema.insert("default".to_owned(), json!(default == "true"));
        } else {
            schema.insert("default".to_owned(), json!(default));
        }
    }

    Value::Object(schema)
}

fn truncate(s: &str, max: usize) -> String {
    if s.len() <= max {
        s.to_owned()
    } else {
        format!("{}...", &s[..max - 3])
    }
}

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

    #[test]
    fn build_tools_returns_expected_count() {
        let tools = build_tools();
        assert_eq!(tools.len(), 13);
    }

    #[test]
    fn tool_names_are_correct() {
        let tools = build_tools();
        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
        let expected = vec![
            "ccd_repo",
            "ccd_health",
            "ccd_session",
            "ccd_session_lifecycle",
            "ccd_session_gates",
            "ccd_escalation",
            "ccd_recovery",
            "ccd_state",
            "ccd_delegation",
            "ccd_context",
            "ccd_memory_capture",
            "ccd_memory",
            "ccd_memory_recall",
        ];
        assert_eq!(names, expected);
    }

    #[test]
    fn budget_limits_hold() {
        let tools = build_tools();
        for tool in &tools {
            let props = tool.input_schema["properties"].as_object().unwrap();
            assert!(
                props.len() <= 10,
                "{} has {} properties",
                tool.name,
                props.len()
            );
            let cmd_enum = tool.input_schema["properties"]["command"]["enum"]
                .as_array()
                .unwrap();
            assert!(
                cmd_enum.len() <= 7,
                "{} has {} commands",
                tool.name,
                cmd_enum.len()
            );
            assert!(
                tool.description.len() <= 80,
                "{} description is {} chars",
                tool.name,
                tool.description.len()
            );
        }
    }

    #[test]
    fn command_is_required() {
        let tools = build_tools();
        for tool in &tools {
            let required = tool.input_schema["required"].as_array().unwrap();
            assert!(
                required.contains(&json!("command")),
                "{} missing required command",
                tool.name
            );
        }
    }

    #[test]
    fn from_ref_renamed_to_from_in_session() {
        let tools = build_tools();
        let session = tools.iter().find(|t| t.name == "ccd_session").unwrap();
        let props = session.input_schema["properties"].as_object().unwrap();
        assert!(props.contains_key("from"), "expected 'from' property");
        assert!(
            !props.contains_key("from_ref"),
            "unexpected 'from_ref' property"
        );
    }

    #[test]
    fn session_mode_is_exposed_but_check_is_omitted() {
        let tools = build_tools();
        let session = tools.iter().find(|t| t.name == "ccd_session").unwrap();
        let props = session.input_schema["properties"].as_object().unwrap();
        assert_eq!(
            session.description,
            "Startup context and shared-checkout helpers: start open state-start state-clear"
        );
        assert!(
            props.contains_key("mode"),
            "mode should be exposed for session-state start"
        );
        assert!(
            !props.contains_key("activate"),
            "activate is CLI-only coalescing flag"
        );
        assert!(
            !props.contains_key("check"),
            "check is CLI-only exit-code mode"
        );
    }

    #[test]
    fn session_lifecycle_tool_bundles_owner_fields() {
        let tools = build_tools();
        let lifecycle = tools
            .iter()
            .find(|t| t.name == "ccd_session_lifecycle")
            .unwrap();
        let props = lifecycle.input_schema["properties"].as_object().unwrap();
        assert!(
            props.contains_key("session_owner"),
            "expected bundled session_owner property"
        );
        assert!(
            !props.contains_key("actor_id"),
            "unexpected top-level actor_id property"
        );
        assert!(
            !props.contains_key("supervisor_id"),
            "unexpected top-level supervisor_id property"
        );
        assert!(
            !props.contains_key("lease_seconds"),
            "unexpected top-level lease_seconds property"
        );
    }

    #[test]
    fn session_gates_tool_bundles_protected_write_fields() {
        let tools = build_tools();
        let gates = tools
            .iter()
            .find(|t| t.name == "ccd_session_gates")
            .unwrap();
        let props = gates.input_schema["properties"].as_object().unwrap();
        assert!(
            props.contains_key("protected_write"),
            "expected bundled protected_write property"
        );
        assert!(
            !props.contains_key("actor_id"),
            "unexpected top-level actor_id property"
        );
        assert!(
            !props.contains_key("session_id"),
            "unexpected top-level session_id property"
        );
        assert!(
            !props.contains_key("expected_revision"),
            "unexpected top-level expected_revision property"
        );
        assert_eq!(props["gates"]["type"], "array");
    }

    #[test]
    fn universally_required_args_in_required_array() {
        let tools = build_tools();
        let memory = tools.iter().find(|t| t.name == "ccd_memory").unwrap();
        let required = memory.input_schema["required"].as_array().unwrap();
        assert!(
            !required.contains(&json!("entry")),
            "entry should not be universally required now that review mode can list candidates"
        );
        // scope is only required by memory-compact, not memory-promote
        assert!(
            !required.contains(&json!("scope")),
            "scope should not be in required"
        );
        assert!(
            !required.contains(&json!("candidate_route")),
            "candidate_route should not be universally required"
        );
    }

    #[test]
    fn core_extension_split_does_not_expose_backlog_tool() {
        let tools = build_tools();
        assert!(tools.iter().all(|tool| tool.name != "ccd_backlog"));
    }
}
