//! ScriptedTool execution: Tool impl, builtin adapter, flag parser, documentation helpers.

use super::{
    ScriptedCommandInvocation, ScriptedCommandKind, ScriptedExecutionTrace, ScriptedTool, ToolArgs,
    ToolCallback,
};
use crate::Bash;
use crate::builtins::{Builtin, Context};
use crate::error::Result;
use crate::interpreter::ExecResult;
use crate::tool::{
    Tool, ToolError, ToolExecution, ToolOutputChunk, ToolRequest, ToolResponse, ToolStatus,
    VERSION, localized, tool_output_from_response, tool_request_from_value,
};
use async_trait::async_trait;
use schemars::schema_for;
use std::sync::{Arc, Mutex};

type InvocationLog = Arc<Mutex<Vec<ScriptedCommandInvocation>>>;

fn push_invocation(
    log: &InvocationLog,
    name: &str,
    kind: ScriptedCommandKind,
    args: &[String],
    exit_code: i32,
) {
    let mut invocations = log.lock().expect("scripted invocation log poisoned");
    invocations.push(ScriptedCommandInvocation {
        name: name.to_string(),
        kind,
        args: args.to_vec(),
        exit_code,
    });
}

// ============================================================================
// Flag parser — `--key value` / `--key=value` → JSON object
// ============================================================================

/// Parse `--key value` and `--key=value` flags into a JSON object.
/// Types are coerced according to the schema's property definitions.
/// Unknown flags (not in schema) are kept as strings.
/// Bare `--flag` without a value is treated as `true` if the schema says boolean,
/// otherwise as `true` when the next arg also starts with `--` or is absent.
fn parse_flags(
    raw_args: &[String],
    schema: &serde_json::Value,
) -> std::result::Result<serde_json::Value, String> {
    let properties = schema
        .get("properties")
        .and_then(|p| p.as_object())
        .cloned()
        .unwrap_or_default();

    let mut result = serde_json::Map::new();
    let mut i = 0;

    while i < raw_args.len() {
        let arg = &raw_args[i];

        let Some(flag) = arg.strip_prefix("--") else {
            return Err(format!("expected --flag, got: {arg}"));
        };

        // --key=value
        if let Some((key, raw_value)) = flag.split_once('=') {
            let value = coerce_value(raw_value, properties.get(key));
            result.insert(key.to_string(), value);
            i += 1;
            continue;
        }

        // --flag (boolean) or --key value
        let key = flag;
        let prop_schema = properties.get(key);
        let is_boolean = prop_schema
            .and_then(|s| s.get("type"))
            .and_then(|t| t.as_str())
            == Some("boolean");

        if is_boolean {
            result.insert(key.to_string(), serde_json::Value::Bool(true));
            i += 1;
        } else if i + 1 < raw_args.len() && !raw_args[i + 1].starts_with("--") {
            let raw_value = &raw_args[i + 1];
            let value = coerce_value(raw_value, prop_schema);
            result.insert(key.to_string(), value);
            i += 2;
        } else {
            // No value follows and not boolean — treat as true
            result.insert(key.to_string(), serde_json::Value::Bool(true));
            i += 1;
        }
    }

    Ok(serde_json::Value::Object(result))
}

/// Coerce a raw string value to the type declared in the property schema.
fn coerce_value(raw: &str, prop_schema: Option<&serde_json::Value>) -> serde_json::Value {
    let type_str = prop_schema
        .and_then(|s| s.get("type"))
        .and_then(|t| t.as_str())
        .unwrap_or("string");

    match type_str {
        "integer" => raw
            .parse::<i64>()
            .map(serde_json::Value::from)
            .unwrap_or_else(|_| serde_json::Value::String(raw.to_string())),
        "number" => raw
            .parse::<f64>()
            .map(|n| serde_json::json!(n))
            .unwrap_or_else(|_| serde_json::Value::String(raw.to_string())),
        "boolean" => match raw {
            "true" | "1" | "yes" => serde_json::Value::Bool(true),
            "false" | "0" | "no" => serde_json::Value::Bool(false),
            _ => serde_json::Value::String(raw.to_string()),
        },
        _ => serde_json::Value::String(raw.to_string()),
    }
}

/// Generate a usage hint from schema properties: `--id <integer> --name <string>`.
fn usage_from_schema(schema: &serde_json::Value) -> Option<String> {
    let props = schema.get("properties")?.as_object()?;
    if props.is_empty() {
        return None;
    }
    let flags: Vec<String> = props
        .iter()
        .map(|(key, prop)| {
            let ty = prop.get("type").and_then(|t| t.as_str()).unwrap_or("value");
            format!("--{key} <{ty}>")
        })
        .collect();
    Some(flags.join(" "))
}

// ============================================================================
// ToolBuiltinAdapter — wraps ToolCallback as a Builtin
// ============================================================================

/// Adapts a [`ToolCallback`] into a [`Builtin`] so the interpreter can execute it.
/// Parses `--key value` flags from `ctx.args` using the schema for type coercion.
struct ToolBuiltinAdapter {
    name: String,
    callback: ToolCallback,
    schema: serde_json::Value,
    log: InvocationLog,
}

#[async_trait]
impl Builtin for ToolBuiltinAdapter {
    async fn execute(&self, ctx: Context<'_>) -> Result<ExecResult> {
        let exit_result = match parse_flags(ctx.args, &self.schema) {
            Ok(params) => {
                let tool_args = ToolArgs {
                    params,
                    stdin: ctx.stdin.map(String::from),
                };

                match (self.callback)(&tool_args) {
                    Ok(stdout) => ExecResult::ok(stdout),
                    Err(msg) => ExecResult::err(msg, 1),
                }
            }
            Err(msg) => ExecResult::err(msg, 2),
        };

        push_invocation(
            &self.log,
            &self.name,
            ScriptedCommandKind::Tool,
            ctx.args,
            exit_result.exit_code,
        );
        Ok(exit_result)
    }
}

// ============================================================================
// HelpBuiltin — runtime schema introspection
// ============================================================================

/// Snapshot of a tool definition for the `help` and `discover` builtins.
#[derive(Clone)]
struct ToolDefSnapshot {
    name: String,
    description: String,
    input_schema: serde_json::Value,
    tags: Vec<String>,
    category: Option<String>,
}

/// Built-in `help` command for runtime tool schema introspection.
///
/// Modes:
/// - `help --list` — list all tool names + descriptions
/// - `help <tool>` — human-readable usage
/// - `help <tool> --json` — machine-readable JSON schema
struct HelpBuiltin {
    tools: Vec<ToolDefSnapshot>,
    log: InvocationLog,
}

#[async_trait]
impl Builtin for HelpBuiltin {
    async fn execute(&self, ctx: Context<'_>) -> Result<ExecResult> {
        let args = ctx.args;

        let result = if args.is_empty() || (args.len() == 1 && args[0] == "--list") {
            // List all tools
            let mut out = String::new();
            for t in &self.tools {
                out.push_str(&format!("{:<20} {}\n", t.name, t.description));
            }
            ExecResult::ok(out)
        } else {
            // Find the tool name (first non-flag arg)
            let tool_name = args.iter().find(|a| !a.starts_with("--"));
            let json_mode = args.iter().any(|a| a == "--json");

            let Some(tool_name) = tool_name else {
                let result =
                    ExecResult::err("usage: help [--list] [<tool>] [--json]".to_string(), 1);
                push_invocation(
                    &self.log,
                    "help",
                    ScriptedCommandKind::Help,
                    args,
                    result.exit_code,
                );
                return Ok(result);
            };

            let Some(tool) = self.tools.iter().find(|t| t.name == *tool_name) else {
                let result = ExecResult::err(format!("help: unknown tool: {tool_name}"), 1);
                push_invocation(
                    &self.log,
                    "help",
                    ScriptedCommandKind::Help,
                    args,
                    result.exit_code,
                );
                return Ok(result);
            };

            if json_mode {
                // Machine-readable JSON output
                let obj = serde_json::json!({
                    "name": tool.name,
                    "description": tool.description,
                    "input_schema": tool.input_schema,
                });
                let json_str = serde_json::to_string_pretty(&obj).unwrap_or_default();
                ExecResult::ok(format!("{json_str}\n"))
            } else {
                // Human-readable output
                let mut out = format!("{} - {}\n", tool.name, tool.description);
                if let Some(usage) = usage_from_schema(&tool.input_schema) {
                    out.push_str(&format!("Usage: {} {}\n", tool.name, usage));
                }
                ExecResult::ok(out)
            }
        };

        push_invocation(
            &self.log,
            "help",
            ScriptedCommandKind::Help,
            args,
            result.exit_code,
        );
        Ok(result)
    }
}

// ============================================================================
// DiscoverBuiltin — progressive tool discovery
// ============================================================================

/// Built-in `discover` command for exploring large tool sets.
struct DiscoverBuiltin {
    tools: Vec<ToolDefSnapshot>,
    log: InvocationLog,
}

impl DiscoverBuiltin {
    fn filter_tools(&self, args: &[String]) -> (Vec<&ToolDefSnapshot>, bool) {
        let json_mode = args.iter().any(|a| a == "--json");

        if args.iter().any(|a| a == "--categories") {
            return (Vec::new(), json_mode);
        }

        if let Some(pos) = args.iter().position(|a| a == "--category") {
            let cat = args.get(pos + 1).map(|s| s.as_str()).unwrap_or("");
            let filtered: Vec<&ToolDefSnapshot> = self
                .tools
                .iter()
                .filter(|t| t.category.as_deref() == Some(cat))
                .collect();
            return (filtered, json_mode);
        }

        if let Some(pos) = args.iter().position(|a| a == "--tag") {
            let tag = args.get(pos + 1).map(|s| s.as_str()).unwrap_or("");
            let filtered: Vec<&ToolDefSnapshot> = self
                .tools
                .iter()
                .filter(|t| t.tags.iter().any(|tg| tg == tag))
                .collect();
            return (filtered, json_mode);
        }

        if let Some(pos) = args.iter().position(|a| a == "--search") {
            let keyword = args
                .get(pos + 1)
                .map(|s| s.to_lowercase())
                .unwrap_or_default();
            let filtered: Vec<&ToolDefSnapshot> = self
                .tools
                .iter()
                .filter(|t| {
                    t.name.to_lowercase().contains(&keyword)
                        || t.description.to_lowercase().contains(&keyword)
                })
                .collect();
            return (filtered, json_mode);
        }

        (self.tools.iter().collect(), json_mode)
    }
}

#[async_trait]
impl Builtin for DiscoverBuiltin {
    async fn execute(&self, ctx: Context<'_>) -> Result<ExecResult> {
        let args = ctx.args;

        let result = if args.is_empty() {
            ExecResult::err(
                "usage: discover --categories | --category <name> | --tag <tag> | --search <keyword> [--json]".to_string(),
                1,
            )
        } else {
            let json_mode = args.iter().any(|a| a == "--json");

            // --categories
            if args.iter().any(|a| a == "--categories") {
                let mut cats: std::collections::BTreeMap<String, usize> =
                    std::collections::BTreeMap::new();
                for t in &self.tools {
                    if let Some(ref cat) = t.category {
                        *cats.entry(cat.clone()).or_insert(0) += 1;
                    }
                }
                if json_mode {
                    let arr: Vec<serde_json::Value> = cats
                        .iter()
                        .map(|(name, count)| serde_json::json!({"category": name, "count": count}))
                        .collect();
                    let json_str =
                        serde_json::to_string_pretty(&arr).unwrap_or_else(|_| "[]".to_string());
                    ExecResult::ok(format!("{json_str}\n"))
                } else {
                    let mut out = String::new();
                    for (name, count) in &cats {
                        let plural = if *count == 1 { "tool" } else { "tools" };
                        out.push_str(&format!("{name} ({count} {plural})\n"));
                    }
                    ExecResult::ok(out)
                }
            } else {
                let (filtered, _) = self.filter_tools(args);

                if json_mode {
                    let arr: Vec<serde_json::Value> = filtered
                        .iter()
                        .map(|t| {
                            let mut obj = serde_json::json!({
                                "name": t.name,
                                "description": t.description,
                            });
                            if !t.tags.is_empty() {
                                obj["tags"] = serde_json::json!(t.tags);
                            }
                            if let Some(ref cat) = t.category {
                                obj["category"] = serde_json::json!(cat);
                            }
                            obj
                        })
                        .collect();
                    let json_str =
                        serde_json::to_string_pretty(&arr).unwrap_or_else(|_| "[]".to_string());
                    ExecResult::ok(format!("{json_str}\n"))
                } else {
                    let mut out = String::new();
                    for t in &filtered {
                        out.push_str(&format!("{:<20} {}\n", t.name, t.description));
                    }
                    ExecResult::ok(out)
                }
            }
        };

        push_invocation(
            &self.log,
            "discover",
            ScriptedCommandKind::Discover,
            args,
            result.exit_code,
        );
        Ok(result)
    }
}

// ============================================================================
// ScriptedTool — internal helpers
// ============================================================================

impl ScriptedTool {
    /// Create a fresh Bash instance with all tool builtins registered.
    fn create_bash(&self, log: InvocationLog) -> Bash {
        let mut builder = Bash::builder();

        if let Some(ref limits) = self.limits {
            builder = builder.limits(limits.clone());
        }
        for (key, value) in &self.env_vars {
            builder = builder.env(key, value);
        }
        for tool in &self.tools {
            let name = tool.def.name.clone();
            let builtin: Box<dyn Builtin> = Box::new(ToolBuiltinAdapter {
                name: name.clone(),
                callback: Arc::clone(&tool.callback),
                schema: tool.def.input_schema.clone(),
                log: Arc::clone(&log),
            });
            builder = builder.builtin(name, builtin);
        }

        // Register the help and discover builtins
        let snapshots: Vec<ToolDefSnapshot> = self
            .tools
            .iter()
            .map(|t| ToolDefSnapshot {
                name: t.def.name.clone(),
                description: t.def.description.clone(),
                input_schema: t.def.input_schema.clone(),
                tags: t.def.tags.clone(),
                category: t.def.category.clone(),
            })
            .collect();
        builder = builder.builtin(
            "help".to_string(),
            Box::new(HelpBuiltin {
                tools: snapshots.clone(),
                log: Arc::clone(&log),
            }),
        );
        builder = builder.builtin(
            "discover".to_string(),
            Box::new(DiscoverBuiltin {
                tools: snapshots,
                log,
            }),
        );

        builder.build()
    }

    fn build_help(&self) -> String {
        let mut doc = format!(
            "# {}\n\n{}\n\n**Version:** {}\n**Name:** `{}`\n**Locale:** `{}`\n\n## Parameters\n\n| Name | Type | Required | Default | Description |\n|------|------|----------|---------|-------------|\n| `commands` | string | yes | — | Bash script that may call the registered tool commands |\n| `timeout_ms` | integer | no | — | Per-call timeout in milliseconds |\n\n## Tool Commands\n\n| Name | Description | Usage |\n|------|-------------|-------|\n",
            self.display_name, self.description, VERSION, self.name, self.locale
        );

        for t in &self.tools {
            let usage = usage_from_schema(&t.def.input_schema)
                .map(|u| format!("`{} {}`", t.def.name, u))
                .unwrap_or_else(|| format!("`{}`", t.def.name));
            doc.push_str(&format!(
                "| `{}` | {} | {} |\n",
                t.def.name, t.def.description, usage
            ));
        }

        doc.push_str(
            "\n## Result\n\n| Field | Type | Description |\n|------|------|-------------|\n| `stdout` | string | Combined standard output |\n| `stderr` | string | Tool or bash errors |\n| `exit_code` | integer | Shell exit code |\n| `error` | string | Error category when execution fails |\n\n## Examples\n\n```json\n{\"commands\":\"get_user --id 42\"}\n```\n\n```json\n{\"commands\":\"user=$(get_user --id 42)\\necho \\\"$user\\\" | jq -r '.name'\"}\n```\n\n## Notes\n\n- Pass arguments as `--key value` or `--key=value`.\n- Standard bash builtins like `echo`, `jq`, `grep`, `sed`, and `awk` are available.\n- Use `help <tool> --json` inside the tool for runtime schema inspection.\n",
        );

        doc
    }

    fn build_system_prompt(&self) -> String {
        let mut parts = vec![format!(
            "{}: {}.",
            self.name,
            localized(
                self.locale.as_str(),
                "run bash scripts that orchestrate registered tool commands",
                "виконує bash-скрипти для оркестрації зареєстрованих команд",
            )
        )];

        let tools = self
            .tools
            .iter()
            .map(|tool| {
                if self.compact_prompt {
                    format!("{} ({})", tool.def.name, tool.def.description)
                } else if let Some(usage) = usage_from_schema(&tool.def.input_schema) {
                    format!("{} [{}]", tool.def.name, usage)
                } else {
                    tool.def.name.clone()
                }
            })
            .collect::<Vec<_>>()
            .join(", ");
        parts.push(format!(
            "{}: {}.",
            localized(self.locale.as_str(), "Commands", "Команди"),
            tools
        ));
        parts.push(localized(
            self.locale.as_str(),
            "Pass args as --key value or --key=value. Use help/discover builtins for runtime details.",
            "Передавайте аргументи як --key value або --key=value. Використовуйте help/discover для деталей.",
        ).to_string());

        parts.join(" ")
    }

    async fn run_request_with_stream(
        &self,
        req: ToolRequest,
        stream_sender: Option<tokio::sync::mpsc::UnboundedSender<ToolOutputChunk>>,
    ) -> ToolResponse {
        if req.commands.is_empty() {
            self.store_last_execution_trace(ScriptedExecutionTrace::default());
            return ToolResponse {
                stdout: String::new(),
                stderr: String::new(),
                exit_code: 0,
                error: None,
                ..Default::default()
            };
        }

        let log: InvocationLog = Arc::new(Mutex::new(Vec::new()));
        let mut bash = self.create_bash(Arc::clone(&log));

        let response = if let Some(sender) = stream_sender {
            let output_cb = Box::new(move |stdout_chunk: &str, stderr_chunk: &str| {
                if !stdout_chunk.is_empty() {
                    let _ = sender.send(ToolOutputChunk {
                        data: serde_json::json!(stdout_chunk),
                        kind: "stdout".to_string(),
                    });
                }
                if !stderr_chunk.is_empty() {
                    let _ = sender.send(ToolOutputChunk {
                        data: serde_json::json!(stderr_chunk),
                        kind: "stderr".to_string(),
                    });
                }
            });
            bash.exec_streaming(&req.commands, output_cb).await
        } else {
            bash.exec(&req.commands).await
        };

        let response = match response {
            Ok(result) => result.into(),
            Err(err) => ToolResponse {
                stdout: String::new(),
                stderr: err.to_string(),
                exit_code: 1,
                error: Some(err.to_string()),
                ..Default::default()
            },
        };
        let invocations = log
            .lock()
            .expect("scripted invocation log poisoned")
            .clone();
        self.store_last_execution_trace(ScriptedExecutionTrace { invocations });
        response
    }
}

// ============================================================================
// Tool trait implementation
// ============================================================================

#[async_trait]
impl Tool for ScriptedTool {
    fn name(&self) -> &str {
        &self.name
    }

    fn display_name(&self) -> &str {
        &self.display_name
    }

    fn short_description(&self) -> &str {
        &self.short_desc
    }

    fn description(&self) -> &str {
        &self.description
    }

    fn help(&self) -> String {
        self.build_help()
    }

    fn system_prompt(&self) -> String {
        self.build_system_prompt()
    }

    fn locale(&self) -> &str {
        &self.locale
    }

    fn input_schema(&self) -> serde_json::Value {
        let schema = schema_for!(ToolRequest);
        serde_json::to_value(schema).unwrap_or_default()
    }

    fn output_schema(&self) -> serde_json::Value {
        let schema = schema_for!(ToolResponse);
        serde_json::to_value(schema).unwrap_or_default()
    }

    fn version(&self) -> &str {
        VERSION
    }

    fn execution(&self, args: serde_json::Value) -> std::result::Result<ToolExecution, ToolError> {
        let req = tool_request_from_value(self.locale(), args)?;
        let tool = self.clone();
        Ok(ToolExecution::new(move |stream_sender| async move {
            let start = std::time::Instant::now();
            let response = tool.run_request_with_stream(req, stream_sender).await;
            tool_output_from_response(response, start.elapsed())
        }))
    }

    async fn execute(&self, req: ToolRequest) -> ToolResponse {
        self.run_request_with_stream(req, None).await
    }

    async fn execute_with_status(
        &self,
        req: ToolRequest,
        mut status_callback: Box<dyn FnMut(ToolStatus) + Send>,
    ) -> ToolResponse {
        status_callback(ToolStatus::new("validate").with_percent(0.0));

        if req.commands.is_empty() {
            status_callback(ToolStatus::new("complete").with_percent(100.0));
            return ToolResponse {
                stdout: String::new(),
                stderr: String::new(),
                exit_code: 0,
                error: None,
                ..Default::default()
            };
        }

        status_callback(ToolStatus::new("parse").with_percent(10.0));
        status_callback(ToolStatus::new("execute").with_percent(20.0));
        let response = self.run_request_with_stream(req, None).await;

        status_callback(ToolStatus::new("complete").with_percent(100.0));
        response
    }
}

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

    #[test]
    fn test_parse_flags_key_value() {
        let schema = serde_json::json!({
            "type": "object",
            "properties": {
                "id": {"type": "integer"},
                "name": {"type": "string"}
            }
        });
        let args = vec!["--id".into(), "42".into(), "--name".into(), "Alice".into()];
        let result = parse_flags(&args, &schema).expect("parse_flags should succeed");
        assert_eq!(result["id"], 42);
        assert_eq!(result["name"], "Alice");
    }

    #[test]
    fn test_parse_flags_equals_syntax() {
        let schema = serde_json::json!({
            "type": "object",
            "properties": { "id": {"type": "integer"} }
        });
        let args = vec!["--id=99".into()];
        let result = parse_flags(&args, &schema).expect("parse_flags should succeed");
        assert_eq!(result["id"], 99);
    }

    #[test]
    fn test_parse_flags_boolean() {
        let schema = serde_json::json!({
            "type": "object",
            "properties": {
                "verbose": {"type": "boolean"},
                "query": {"type": "string"}
            }
        });
        let args = vec!["--verbose".into(), "--query".into(), "hello".into()];
        let result = parse_flags(&args, &schema).expect("parse_flags should succeed");
        assert_eq!(result["verbose"], true);
        assert_eq!(result["query"], "hello");
    }

    #[test]
    fn test_parse_flags_no_schema() {
        let schema = serde_json::json!({});
        let args = vec!["--name".into(), "Bob".into()];
        let result = parse_flags(&args, &schema).expect("parse_flags should succeed");
        assert_eq!(result["name"], "Bob");
    }

    #[test]
    fn test_parse_flags_empty() {
        let schema = serde_json::json!({});
        let result = parse_flags(&[], &schema).expect("parse_flags should succeed");
        assert_eq!(result, serde_json::json!({}));
    }

    #[test]
    fn test_parse_flags_rejects_positional() {
        let schema = serde_json::json!({});
        let result = parse_flags(&["42".into()], &schema);
        assert!(result.is_err());
        assert!(
            result
                .expect_err("should reject positional")
                .contains("expected --flag")
        );
    }

    #[test]
    fn test_usage_from_schema() {
        let schema = serde_json::json!({
            "type": "object",
            "properties": {
                "id": {"type": "integer"},
                "name": {"type": "string"}
            }
        });
        let usage = usage_from_schema(&schema).expect("should produce usage string");
        assert!(usage.contains("--id <integer>"));
        assert!(usage.contains("--name <string>"));
    }

    #[test]
    fn test_usage_from_empty_schema() {
        assert!(usage_from_schema(&serde_json::json!({})).is_none());
        assert!(
            usage_from_schema(&serde_json::json!({"type": "object", "properties": {}})).is_none()
        );
    }

    // -- HelpBuiltin tests --

    fn build_help_test_tool() -> ScriptedTool {
        ScriptedTool::builder("test_api")
            .short_description("Test API")
            .tool(
                ToolDef::new("get_user", "Fetch user by ID").with_schema(serde_json::json!({
                    "type": "object",
                    "properties": {
                        "id": {"type": "integer"}
                    }
                })),
                |_args: &super::ToolArgs| Ok("{\"id\":1}\n".to_string()),
            )
            .tool(
                ToolDef::new("list_orders", "List orders for user").with_schema(
                    serde_json::json!({
                        "type": "object",
                        "properties": {
                            "user_id": {"type": "integer"},
                            "limit": {"type": "integer"}
                        }
                    }),
                ),
                |_args: &super::ToolArgs| Ok("[]\n".to_string()),
            )
            .build()
    }

    #[tokio::test]
    async fn test_help_list() {
        let tool = build_help_test_tool();
        let resp = tool
            .execute(ToolRequest {
                commands: "help --list".to_string(),
                timeout_ms: None,
            })
            .await;
        assert_eq!(resp.exit_code, 0);
        assert!(resp.stdout.contains("get_user"));
        assert!(resp.stdout.contains("Fetch user by ID"));
        assert!(resp.stdout.contains("list_orders"));
    }

    #[tokio::test]
    async fn test_help_tool_human_readable() {
        let tool = build_help_test_tool();
        let resp = tool
            .execute(ToolRequest {
                commands: "help get_user".to_string(),
                timeout_ms: None,
            })
            .await;
        assert_eq!(resp.exit_code, 0);
        assert!(resp.stdout.contains("get_user - Fetch user by ID"));
        assert!(resp.stdout.contains("--id <integer>"));
    }

    #[tokio::test]
    async fn test_help_tool_json() {
        let tool = build_help_test_tool();
        let resp = tool
            .execute(ToolRequest {
                commands: "help get_user --json".to_string(),
                timeout_ms: None,
            })
            .await;
        assert_eq!(resp.exit_code, 0);
        let parsed: serde_json::Value =
            serde_json::from_str(resp.stdout.trim()).expect("should be valid JSON");
        assert_eq!(parsed["name"], "get_user");
        assert_eq!(parsed["description"], "Fetch user by ID");
        assert!(parsed["input_schema"]["properties"]["id"].is_object());
    }

    #[tokio::test]
    async fn test_help_unknown_tool() {
        let tool = build_help_test_tool();
        let resp = tool
            .execute(ToolRequest {
                commands: "help nonexistent".to_string(),
                timeout_ms: None,
            })
            .await;
        assert_ne!(resp.exit_code, 0);
        assert!(resp.stderr.contains("unknown tool"));
    }

    #[tokio::test]
    async fn test_help_no_args_lists_all() {
        let tool = build_help_test_tool();
        let resp = tool
            .execute(ToolRequest {
                commands: "help".to_string(),
                timeout_ms: None,
            })
            .await;
        assert_eq!(resp.exit_code, 0);
        assert!(resp.stdout.contains("get_user"));
        assert!(resp.stdout.contains("list_orders"));
    }

    #[tokio::test]
    async fn test_help_json_pipe_jq() {
        let tool = build_help_test_tool();
        let resp = tool
            .execute(ToolRequest {
                commands: "help get_user --json | jq -r '.name'".to_string(),
                timeout_ms: None,
            })
            .await;
        assert_eq!(resp.exit_code, 0);
        assert_eq!(resp.stdout.trim(), "get_user");
    }

    #[tokio::test]
    async fn test_compact_prompt_omits_usage() {
        let tool = ScriptedTool::builder("compact_test")
            .compact_prompt(true)
            .tool(
                ToolDef::new("get_user", "Fetch user").with_schema(serde_json::json!({
                    "type": "object",
                    "properties": { "id": {"type": "integer"} }
                })),
                |_args: &super::ToolArgs| Ok("ok\n".to_string()),
            )
            .build();
        let sp = tool.system_prompt();
        assert!(sp.contains("help/discover"));
        assert!(!sp.contains("Usage:"));
    }

    #[tokio::test]
    async fn test_non_compact_prompt_has_usage() {
        let tool = ScriptedTool::builder("full_test")
            .tool(
                ToolDef::new("get_user", "Fetch user").with_schema(serde_json::json!({
                    "type": "object",
                    "properties": { "id": {"type": "integer"} }
                })),
                |_args: &super::ToolArgs| Ok("ok\n".to_string()),
            )
            .build();
        let sp = tool.system_prompt();
        assert!(sp.contains("--id <integer>"));
    }

    #[tokio::test]
    async fn test_error_uses_display_not_debug() {
        use super::ScriptedTool;
        use crate::ToolDef;
        use crate::tool::Tool;

        let tool = ScriptedTool::builder("test")
            .short_description("test")
            .tool(
                ToolDef::new("fail", "Always fails"),
                |_args: &super::ToolArgs| Err("service error".to_string()),
            )
            .build();
        let req = ToolRequest {
            commands: "fail".into(),
            timeout_ms: None,
        };
        let resp = tool.execute(req).await;
        // Error messages use Display format, not Debug, to avoid leaking internals
        if let Some(ref err) = resp.error {
            assert!(
                !err.contains("Execution("),
                "error should use Display not Debug: {err}",
            );
        }
    }

    // -- DiscoverBuiltin tests --

    fn build_discover_test_tool() -> ScriptedTool {
        ScriptedTool::builder("big_api")
            .short_description("Big API")
            .tool(
                ToolDef::new("create_charge", "Create a payment charge")
                    .with_category("payments")
                    .with_tags(&["billing", "write"]),
                |_args: &super::ToolArgs| Ok("ok\n".to_string()),
            )
            .tool(
                ToolDef::new("refund", "Issue a refund")
                    .with_category("payments")
                    .with_tags(&["billing", "write"]),
                |_args: &super::ToolArgs| Ok("ok\n".to_string()),
            )
            .tool(
                ToolDef::new("get_user", "Fetch user by ID")
                    .with_category("users")
                    .with_tags(&["read"]),
                |_args: &super::ToolArgs| Ok("ok\n".to_string()),
            )
            .tool(
                ToolDef::new("delete_user", "Delete a user account")
                    .with_category("users")
                    .with_tags(&["admin", "write"]),
                |_args: &super::ToolArgs| Ok("ok\n".to_string()),
            )
            .tool(
                ToolDef::new("get_inventory", "Check inventory levels").with_category("inventory"),
                |_args: &super::ToolArgs| Ok("ok\n".to_string()),
            )
            .build()
    }

    #[tokio::test]
    async fn test_discover_categories() {
        let tool = build_discover_test_tool();
        let resp = tool
            .execute(ToolRequest {
                commands: "discover --categories".to_string(),
                timeout_ms: None,
            })
            .await;
        assert_eq!(resp.exit_code, 0);
        assert!(resp.stdout.contains("payments (2 tools)"));
        assert!(resp.stdout.contains("users (2 tools)"));
        assert!(resp.stdout.contains("inventory (1 tool)"));
    }

    #[tokio::test]
    async fn test_discover_category_filter() {
        let tool = build_discover_test_tool();
        let resp = tool
            .execute(ToolRequest {
                commands: "discover --category payments".to_string(),
                timeout_ms: None,
            })
            .await;
        assert_eq!(resp.exit_code, 0);
        assert!(resp.stdout.contains("create_charge"));
        assert!(resp.stdout.contains("refund"));
        assert!(!resp.stdout.contains("get_user"));
    }

    #[tokio::test]
    async fn test_discover_tag_filter() {
        let tool = build_discover_test_tool();
        let resp = tool
            .execute(ToolRequest {
                commands: "discover --tag admin".to_string(),
                timeout_ms: None,
            })
            .await;
        assert_eq!(resp.exit_code, 0);
        assert!(resp.stdout.contains("delete_user"));
        assert!(!resp.stdout.contains("create_charge"));
    }

    #[tokio::test]
    async fn test_discover_search() {
        let tool = build_discover_test_tool();
        let resp = tool
            .execute(ToolRequest {
                commands: "discover --search user".to_string(),
                timeout_ms: None,
            })
            .await;
        assert_eq!(resp.exit_code, 0);
        assert!(resp.stdout.contains("get_user"));
        assert!(resp.stdout.contains("delete_user"));
        assert!(!resp.stdout.contains("create_charge"));
    }

    #[tokio::test]
    async fn test_discover_search_case_insensitive() {
        let tool = build_discover_test_tool();
        let resp = tool
            .execute(ToolRequest {
                commands: "discover --search REFUND".to_string(),
                timeout_ms: None,
            })
            .await;
        assert_eq!(resp.exit_code, 0);
        assert!(resp.stdout.contains("refund"));
    }

    #[tokio::test]
    async fn test_discover_categories_json() {
        let tool = build_discover_test_tool();
        let resp = tool
            .execute(ToolRequest {
                commands: "discover --categories --json".to_string(),
                timeout_ms: None,
            })
            .await;
        assert_eq!(resp.exit_code, 0);
        let arr: Vec<serde_json::Value> =
            serde_json::from_str(resp.stdout.trim()).expect("valid JSON");
        assert!(
            arr.iter()
                .any(|v| v["category"] == "payments" && v["count"] == 2)
        );
    }

    #[tokio::test]
    async fn test_discover_category_json() {
        let tool = build_discover_test_tool();
        let resp = tool
            .execute(ToolRequest {
                commands: "discover --category payments --json".to_string(),
                timeout_ms: None,
            })
            .await;
        assert_eq!(resp.exit_code, 0);
        let arr: Vec<serde_json::Value> =
            serde_json::from_str(resp.stdout.trim()).expect("valid JSON");
        assert_eq!(arr.len(), 2);
        assert!(arr.iter().any(|v| v["name"] == "create_charge"));
    }

    #[tokio::test]
    async fn test_discover_no_args_shows_usage() {
        let tool = build_discover_test_tool();
        let resp = tool
            .execute(ToolRequest {
                commands: "discover".to_string(),
                timeout_ms: None,
            })
            .await;
        assert_ne!(resp.exit_code, 0);
        assert!(resp.stderr.contains("usage:"));
    }

    #[tokio::test]
    async fn test_discover_tag_json() {
        let tool = build_discover_test_tool();
        let resp = tool
            .execute(ToolRequest {
                commands: "discover --tag billing --json".to_string(),
                timeout_ms: None,
            })
            .await;
        assert_eq!(resp.exit_code, 0);
        let arr: Vec<serde_json::Value> =
            serde_json::from_str(resp.stdout.trim()).expect("valid JSON");
        assert_eq!(arr.len(), 2);
        assert!(arr.iter().all(|v| {
            v["tags"]
                .as_array()
                .expect("tags array")
                .contains(&serde_json::json!("billing"))
        }));
    }

    #[tokio::test]
    async fn test_tooldef_with_tags_and_category() {
        let def = ToolDef::new("test", "A test tool")
            .with_tags(&["admin", "billing"])
            .with_category("payments");
        assert_eq!(def.tags, vec!["admin", "billing"]);
        assert_eq!(def.category.as_deref(), Some("payments"));
    }
}
