//! Scripted tool
//!
//! Compose tool definitions + callbacks into a single [`Tool`] that accepts bash
//! scripts. Each registered tool becomes a builtin command inside the interpreter,
//! so an LLM can orchestrate many operations in one call using pipes, variables,
//! loops, and conditionals.
//!
//! This module follows the same contract surface as [`crate::tool`]:
//!
//! - [`ScriptedToolBuilder::build`] -> immutable metadata object
//! - [`ScriptedToolBuilder::build_service`] -> `tower::Service<Value, Value>`
//! - [`Tool::execution`] -> validated, single-use [`crate::ToolExecution`]
//! - [`Tool::help`] -> Markdown docs
//! - [`Tool::system_prompt`] -> terse plain-text instructions
//!
//! # Architecture
//!
//! ```text
//! ┌─────────────────────────────────────────┐
//! │  ScriptedTool  (implements Tool)        │
//! │                                         │
//! │  ┌─────────┐ ┌─────────┐ ┌──────────┐  │
//! │  │get_user │ │get_order│ │inventory │  │
//! │  │(builtin)│ │(builtin)│ │(builtin) │  │
//! │  └─────────┘ └─────────┘ └──────────┘  │
//! │        ↑           ↑           ↑        │
//! │  bash script: pipes, vars, jq, loops    │
//! └─────────────────────────────────────────┘
//! ```
//!
//! # Example
//!
//! ```rust
//! use bashkit::{ScriptedTool, Tool, ToolArgs, ToolDef};
//!
//! # tokio_test::block_on(async {
//! let tool = ScriptedTool::builder("api")
//!     .tool(
//!         ToolDef::new("greet", "Greet a user")
//!             .with_schema(serde_json::json!({
//!                 "type": "object",
//!                 "properties": { "name": {"type": "string"} }
//!             })),
//!         |args: &ToolArgs| {
//!             let name = args.param_str("name").unwrap_or("world");
//!             Ok(format!("hello {name}\n"))
//!         },
//!     )
//!     .build();
//!
//! let output = tool
//!     .execution(serde_json::json!({"commands": "greet --name Alice"}))
//!     .expect("valid args")
//!     .execute()
//!     .await
//!     .expect("execution succeeds");
//!
//! assert_eq!(output.result["stdout"], "hello Alice\n");
//! assert!(tool.help().contains("## Tool Commands"));
//! # });
//! ```
//!
//! # Shared context across callbacks
//!
//! When multiple tool callbacks need shared resources (HTTP clients, auth tokens,
//! config), use the standard Rust closure-capture pattern with `Arc`:
//!
//! ```rust
//! use bashkit::{ScriptedTool, ToolArgs, ToolDef};
//! use std::sync::Arc;
//!
//! let api_key = Arc::new("sk-secret-key".to_string());
//! let base_url = Arc::new("https://api.example.com".to_string());
//!
//! let k = api_key.clone();
//! let u = base_url.clone();
//! let mut builder = ScriptedTool::builder("api");
//! builder = builder.tool(
//!     ToolDef::new("get_user", "Fetch user by ID"),
//!     move |args: &ToolArgs| {
//!         let _key = &*k;   // shared API key
//!         let _url = &*u;   // shared base URL
//!         Ok(format!("{{\"id\":1}}\n"))
//!     },
//! );
//!
//! let k2 = api_key.clone();
//! let u2 = base_url.clone();
//! builder = builder.tool(
//!     ToolDef::new("list_orders", "List orders"),
//!     move |_args: &ToolArgs| {
//!         let _key = &*k2;
//!         let _url = &*u2;
//!         Ok(format!("[]\n"))
//!     },
//! );
//! let _tool = builder.build();
//! ```
//!
//! For mutable shared state, use `Arc<Mutex<T>>`:
//!
//! ```rust
//! use bashkit::{ScriptedTool, ToolArgs, ToolDef};
//! use std::sync::{Arc, Mutex};
//!
//! let call_count = Arc::new(Mutex::new(0u64));
//! let c = call_count.clone();
//! let tool = ScriptedTool::builder("api")
//!     .tool(
//!         ToolDef::new("tracked", "Counted call"),
//!         move |_args: &ToolArgs| {
//!             let mut count = c.lock().unwrap();
//!             *count += 1;
//!             Ok(format!("call #{count}\n"))
//!         },
//!     )
//!     .build();
//! ```
//!
//! # State across execute() calls
//!
//! Each `execute()` creates a fresh Bash interpreter — no state carries over.
//! This is a security feature (clean sandbox per call). The LLM carries state
//! between calls via its context window: it sees stdout from each call and can
//! pass relevant data from one call's output into the next call's script.
//!
//! For persistent state across calls via callbacks, use `Arc` in closures —
//! the same `Arc<ToolCallback>` instances are reused across `execute()` calls.

mod execute;
mod toolset;

pub use toolset::{DiscoveryMode, ScriptingToolSet, ScriptingToolSetBuilder};

use crate::{ExecutionLimits, Tool, ToolService};
use schemars::schema_for;
use serde::{Deserialize, Serialize};
use std::sync::{Arc, Mutex};

// ============================================================================
// ToolDef — OpenAPI-style tool definition
// ============================================================================

/// OpenAPI-style tool definition: name, description, input schema.
///
/// Describes a sub-tool registered with [`ScriptedToolBuilder`].
/// The `input_schema` is optional JSON Schema for documentation / LLM prompts
/// and for type coercion of `--key value` flags.
#[derive(Clone)]
pub struct ToolDef {
    /// Command name used as bash builtin (e.g. `"get_user"`).
    pub name: String,
    /// Human-readable description for LLM consumption.
    pub description: String,
    /// JSON Schema describing accepted arguments. Empty object if unspecified.
    pub input_schema: serde_json::Value,
    /// Categorical tags for discovery (e.g. `["admin", "billing"]`).
    pub tags: Vec<String>,
    /// Grouping category for discovery (e.g. `"payments"`).
    pub category: Option<String>,
}

impl ToolDef {
    /// Create a tool definition with name and description.
    pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
        Self {
            name: name.into(),
            description: description.into(),
            input_schema: serde_json::Value::Object(Default::default()),
            tags: Vec::new(),
            category: None,
        }
    }

    /// Attach a JSON Schema for the tool's input parameters.
    pub fn with_schema(mut self, schema: serde_json::Value) -> Self {
        self.input_schema = schema;
        self
    }

    /// Add categorical tags for discovery filtering.
    pub fn with_tags(mut self, tags: &[&str]) -> Self {
        self.tags = tags.iter().map(|s| s.to_string()).collect();
        self
    }

    /// Set the grouping category for discovery.
    pub fn with_category(mut self, category: &str) -> Self {
        self.category = Some(category.to_string());
        self
    }
}

// ============================================================================
// ToolArgs — parsed arguments passed to callbacks
// ============================================================================

/// Parsed arguments passed to a tool callback.
///
/// `params` is a JSON object built from `--key value` flags, with values
/// type-coerced per the `ToolDef`'s `input_schema`.
/// `stdin` carries pipeline input from a prior command, if any.
pub struct ToolArgs {
    /// Parsed parameters as a JSON object. Keys from `--key value` flags.
    pub params: serde_json::Value,
    /// Pipeline input from a prior command (e.g. `echo data | tool`).
    pub stdin: Option<String>,
}

impl ToolArgs {
    /// Get a string parameter by name.
    pub fn param_str(&self, key: &str) -> Option<&str> {
        self.params.get(key).and_then(|v| v.as_str())
    }

    /// Get an integer parameter by name.
    pub fn param_i64(&self, key: &str) -> Option<i64> {
        self.params.get(key).and_then(|v| v.as_i64())
    }

    /// Get a float parameter by name.
    pub fn param_f64(&self, key: &str) -> Option<f64> {
        self.params.get(key).and_then(|v| v.as_f64())
    }

    /// Get a boolean parameter by name.
    pub fn param_bool(&self, key: &str) -> Option<bool> {
        self.params.get(key).and_then(|v| v.as_bool())
    }
}

// ============================================================================
// ToolCallback — execution callback type
// ============================================================================

/// Execution callback for a registered tool.
///
/// Receives parsed [`ToolArgs`] with typed parameters and optional stdin.
/// Return `Ok(stdout)` on success or `Err(message)` on failure.
pub type ToolCallback = Arc<dyn Fn(&ToolArgs) -> Result<String, String> + Send + Sync>;

// ============================================================================
// Execution trace — inner scripted command/builtin usage
// ============================================================================

/// Kind of inner command invocation recorded during a `ScriptedTool` execute.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ScriptedCommandKind {
    Tool,
    Help,
    Discover,
}

/// One builtin/tool invocation inside a scripted tool execute.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ScriptedCommandInvocation {
    pub name: String,
    pub kind: ScriptedCommandKind,
    pub args: Vec<String>,
    pub exit_code: i32,
}

/// Inner execution trace captured for the last `ScriptedTool::execute()` call.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ScriptedExecutionTrace {
    pub invocations: Vec<ScriptedCommandInvocation>,
}

// ============================================================================
// RegisteredTool — internal definition + callback pair
// ============================================================================

/// A registered tool: definition + callback.
#[derive(Clone)]
pub(crate) struct RegisteredTool {
    pub(crate) def: ToolDef,
    pub(crate) callback: ToolCallback,
}

// ============================================================================
// ScriptedToolBuilder
// ============================================================================

/// Builder for [`ScriptedTool`].
///
/// ```rust
/// use bashkit::{ScriptedTool, ToolArgs, ToolDef};
///
/// let tool = ScriptedTool::builder("net")
///     .short_description("Network tools")
///     .tool(
///         ToolDef::new("ping", "Ping a host")
///             .with_schema(serde_json::json!({
///                 "type": "object",
///                 "properties": { "host": {"type": "string"} }
///             })),
///         |args: &ToolArgs| {
///             Ok(format!("pong {}\n", args.param_str("host").unwrap_or("?")))
///         },
///     )
///     .build();
/// ```
pub struct ScriptedToolBuilder {
    name: String,
    locale: String,
    short_desc: Option<String>,
    tools: Vec<RegisteredTool>,
    limits: Option<ExecutionLimits>,
    env_vars: Vec<(String, String)>,
    compact_prompt: bool,
}

impl ScriptedToolBuilder {
    pub(crate) fn new(name: impl Into<String>) -> Self {
        Self {
            name: name.into(),
            locale: "en-US".to_string(),
            short_desc: None,
            tools: Vec::new(),
            limits: None,
            env_vars: Vec::new(),
            compact_prompt: false,
        }
    }

    /// Set locale for descriptions, help, prompts, and user-facing errors.
    pub fn locale(mut self, locale: &str) -> Self {
        self.locale = locale.to_string();
        self
    }

    /// One-line description for tool listings.
    pub fn short_description(mut self, desc: impl Into<String>) -> Self {
        self.short_desc = Some(desc.into());
        self
    }

    /// Register a tool with its definition and execution callback.
    ///
    /// The callback receives [`ToolArgs`] with `--key value` flags parsed into
    /// a JSON object, type-coerced per the schema.
    pub fn tool(
        mut self,
        def: ToolDef,
        callback: impl Fn(&ToolArgs) -> Result<String, String> + Send + Sync + 'static,
    ) -> Self {
        self.tools.push(RegisteredTool {
            def,
            callback: Arc::new(callback),
        });
        self
    }

    /// Set execution limits for the bash interpreter.
    pub fn limits(mut self, limits: ExecutionLimits) -> Self {
        self.limits = Some(limits);
        self
    }

    /// Add an environment variable visible inside scripts.
    pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
        self.env_vars.push((key.into(), value.into()));
        self
    }

    /// Emit compact `system_prompt()` that omits full schemas and adds help tip.
    ///
    /// When enabled, `system_prompt()` lists only tool names + one-liners and
    /// instructs the LLM to use `help <tool>` / `help <tool> --json` for details.
    /// Default: `false` (full schemas in prompt, backward compatible).
    pub fn compact_prompt(mut self, compact: bool) -> Self {
        self.compact_prompt = compact;
        self
    }

    /// Build the [`ScriptedTool`].
    pub fn build(&self) -> ScriptedTool {
        let short_desc = self
            .short_desc
            .clone()
            .unwrap_or_else(|| format!("ScriptedTool: {}", self.name));
        let tool_names = self
            .tools
            .iter()
            .map(|tool| tool.def.name.as_str())
            .collect::<Vec<_>>()
            .join(", ");

        ScriptedTool {
            name: self.name.clone(),
            locale: self.locale.clone(),
            display_name: self.name.clone(),
            short_desc,
            description: format!(
                "{}: {}",
                super::tool::localized(
                    self.locale.as_str(),
                    "Compose tool callbacks through bash scripts",
                    "Компонує виклики інструментів через bash-скрипти",
                ),
                tool_names
            ),
            tools: self.tools.clone(),
            limits: self.limits.clone(),
            env_vars: self.env_vars.clone(),
            compact_prompt: self.compact_prompt,
            last_execution_trace: Arc::new(Mutex::new(None)),
        }
    }

    /// Build a `tower::Service<Value, Response = Value, Error = ToolError>`.
    pub fn build_service(&self) -> ToolService {
        let tool = self.build();
        tower::util::BoxCloneService::new(tower::service_fn(move |args| {
            let tool = tool.clone();
            async move {
                let execution = tool.execution(args)?;
                let output = execution.execute().await?;
                Ok(output.result)
            }
        }))
    }

    /// Build an OpenAI-compatible tool definition.
    pub fn build_tool_definition(&self) -> serde_json::Value {
        let tool = self.build();
        serde_json::json!({
            "type": "function",
            "function": {
                "name": tool.name(),
                "description": tool.description(),
                "parameters": self.build_input_schema(),
            }
        })
    }

    /// Build the input schema without constructing the full tool.
    pub fn build_input_schema(&self) -> serde_json::Value {
        let schema = schema_for!(crate::tool::ToolRequest);
        serde_json::to_value(schema).unwrap_or_default()
    }

    /// Build the output schema for `ToolOutput::result`.
    pub fn build_output_schema(&self) -> serde_json::Value {
        let schema = schema_for!(crate::tool::ToolResponse);
        serde_json::to_value(schema).unwrap_or_default()
    }
}

// ============================================================================
// ScriptedTool
// ============================================================================

/// A [`Tool`] that orchestrates multiple tools via bash scripts.
///
/// Each registered tool (defined by [`ToolDef`] + callback) becomes a bash builtin.
/// The LLM sends a bash script that can pipe, loop, branch, and compose these
/// builtins together with standard utilities like `jq`, `grep`, `sed`, etc.
///
/// Arguments are passed as `--key value` flags and parsed into typed JSON
/// per the tool's `input_schema`.
///
/// Reusable — `execute()` can be called multiple times. Each call gets a fresh
/// Bash interpreter with the same set of tool builtins.
///
/// Create via [`ScriptedTool::builder`].
#[derive(Clone)]
pub struct ScriptedTool {
    pub(crate) name: String,
    pub(crate) locale: String,
    pub(crate) display_name: String,
    pub(crate) short_desc: String,
    pub(crate) description: String,
    pub(crate) tools: Vec<RegisteredTool>,
    pub(crate) limits: Option<ExecutionLimits>,
    pub(crate) env_vars: Vec<(String, String)>,
    pub(crate) compact_prompt: bool,
    pub(crate) last_execution_trace: Arc<Mutex<Option<ScriptedExecutionTrace>>>,
}

impl ScriptedTool {
    /// Create a builder with the given tool name.
    pub fn builder(name: impl Into<String>) -> ScriptedToolBuilder {
        ScriptedToolBuilder::new(name)
    }

    /// Return and clear the trace from the most recent execute call.
    pub fn take_last_execution_trace(&self) -> Option<ScriptedExecutionTrace> {
        self.last_execution_trace
            .lock()
            .expect("scripted execution trace poisoned")
            .take()
    }

    pub(crate) fn store_last_execution_trace(&self, trace: ScriptedExecutionTrace) {
        *self
            .last_execution_trace
            .lock()
            .expect("scripted execution trace poisoned") = Some(trace);
    }
}

// ============================================================================
// Tests
// ============================================================================

#[cfg(test)]
mod tests {
    use super::*;
    use crate::tool::{Tool, ToolRequest, VERSION};

    fn build_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: &ToolArgs| {
                    let id = args.param_i64("id").ok_or("missing --id")?;
                    Ok(format!(
                        "{{\"id\":{id},\"name\":\"Alice\",\"email\":\"alice@example.com\"}}\n"
                    ))
                },
            )
            .tool(
                ToolDef::new("get_orders", "List orders for user").with_schema(serde_json::json!({
                    "type": "object",
                    "properties": {
                        "user_id": {"type": "integer"}
                    }
                })),
                |args: &ToolArgs| {
                    let uid = args.param_i64("user_id").ok_or("missing --user_id")?;
                    Ok(format!(
                        "[{{\"order_id\":1,\"user_id\":{uid},\"total\":29.99}},\
                         {{\"order_id\":2,\"user_id\":{uid},\"total\":49.50}}]\n"
                    ))
                },
            )
            .tool(
                ToolDef::new("fail_tool", "Always fails"),
                |_args: &ToolArgs| Err("service unavailable".to_string()),
            )
            .tool(
                ToolDef::new("from_stdin", "Read from stdin, uppercase it"),
                |args: &ToolArgs| match args.stdin.as_deref() {
                    Some(input) => Ok(input.to_uppercase()),
                    None => Err("no stdin".to_string()),
                },
            )
            .build()
    }

    // -- Builder tests --

    #[test]
    fn test_builder_name_and_description() {
        let tool = build_test_tool();
        assert_eq!(tool.name(), "test_api");
        assert_eq!(tool.short_description(), "Test API");
    }

    #[test]
    fn test_builder_default_short_description() {
        let tool = ScriptedTool::builder("mytools")
            .tool(ToolDef::new("noop", "No-op"), |_args: &ToolArgs| {
                Ok("ok\n".to_string())
            })
            .build();
        assert_eq!(tool.short_description(), "ScriptedTool: mytools");
    }

    #[test]
    fn test_description_lists_tools() {
        let tool = build_test_tool();
        let desc = tool.description();
        assert!(desc.contains("get_user"));
        assert!(desc.contains("get_orders"));
        assert!(desc.contains("fail_tool"));
        assert!(desc.contains("from_stdin"));
    }

    #[test]
    fn test_help_has_tool_commands_section() {
        let tool = build_test_tool();
        let help = tool.help();
        assert!(help.contains("## Tool Commands"));
        assert!(help.contains("get_user"));
        assert!(help.contains("Fetch user by id"));
    }

    #[test]
    fn test_system_prompt_lists_tools() {
        let tool = build_test_tool();
        let sp = tool.system_prompt();
        assert!(sp.starts_with("test_api:"));
        assert!(sp.contains("get_user"));
        assert!(sp.contains("get_orders"));
        assert!(sp.contains("--key value"));
    }

    #[test]
    fn test_system_prompt_includes_schema() {
        let tool = ScriptedTool::builder("schema_test")
            .tool(
                ToolDef::new("get_user", "Fetch user by id").with_schema(serde_json::json!({
                    "type": "object",
                    "properties": {
                        "id": {"type": "integer"}
                    },
                    "required": ["id"]
                })),
                |_args: &ToolArgs| Ok("ok\n".to_string()),
            )
            .build();
        let sp = tool.system_prompt();
        assert!(
            sp.contains("--id <integer>"),
            "system prompt should show flags"
        );
    }

    #[test]
    fn test_schemas() {
        let tool = build_test_tool();
        let input = tool.input_schema();
        assert!(input["properties"]["commands"].is_object());
        let output = tool.output_schema();
        assert!(output["properties"]["stdout"].is_object());
    }

    #[test]
    fn test_builder_contract_helpers() {
        let builder = ScriptedTool::builder("test_api")
            .tool(ToolDef::new("ping", "Ping"), |_args: &ToolArgs| {
                Ok("pong\n".to_string())
            });
        let definition = builder.build_tool_definition();
        let input_schema = builder.build_input_schema();
        let output_schema = builder.build_output_schema();

        assert_eq!(definition["type"], "function");
        assert_eq!(definition["function"]["name"], "test_api");
        assert_eq!(definition["function"]["parameters"], input_schema);
        assert!(output_schema["properties"]["stdout"].is_object());
    }

    #[tokio::test]
    async fn test_builder_service_executes() {
        use tower::ServiceExt;

        let service = ScriptedTool::builder("test_api")
            .tool(ToolDef::new("ping", "Ping"), |_args: &ToolArgs| {
                Ok("pong\n".to_string())
            })
            .build_service();

        let result = service
            .oneshot(serde_json::json!({"commands": "ping"}))
            .await
            .unwrap_or_else(|err| panic!("service should execute: {err}"));

        assert_eq!(result["stdout"], "pong\n");
        assert_eq!(result["exit_code"], 0);
    }

    #[test]
    fn test_locale_localizes_description() {
        let tool = ScriptedTool::builder("ua_api")
            .locale("uk-UA")
            .tool(ToolDef::new("ping", "Ping"), |_args: &ToolArgs| {
                Ok("pong\n".to_string())
            })
            .build();

        assert!(tool.description().contains("Компонує"));
        assert_eq!(tool.locale(), "uk-UA");
    }

    #[test]
    fn test_version() {
        let tool = build_test_tool();
        assert_eq!(tool.version(), VERSION);
    }

    // -- Execution tests --

    #[tokio::test]
    async fn test_execute_empty() {
        let tool = build_test_tool();
        let resp = tool
            .execute(ToolRequest {
                commands: String::new(),
                timeout_ms: None,
            })
            .await;
        assert_eq!(resp.exit_code, 0);
        assert!(resp.stdout.is_empty());
    }

    #[tokio::test]
    async fn test_execute_single_tool() {
        let tool = build_test_tool();
        let resp = tool
            .execute(ToolRequest {
                commands: "get_user --id 42".to_string(),
                timeout_ms: None,
            })
            .await;
        assert_eq!(resp.exit_code, 0);
        assert!(resp.stdout.contains("\"name\":\"Alice\""));
        assert!(resp.stdout.contains("\"id\":42"));
    }

    #[tokio::test]
    async fn test_execute_key_equals_value() {
        let tool = build_test_tool();
        let resp = tool
            .execute(ToolRequest {
                commands: "get_user --id=42".to_string(),
                timeout_ms: None,
            })
            .await;
        assert_eq!(resp.exit_code, 0);
        assert!(resp.stdout.contains("\"id\":42"));
    }

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

    #[tokio::test]
    async fn test_execute_multi_step() {
        let tool = build_test_tool();
        let script = r#"
            user=$(get_user --id 1)
            name=$(echo "$user" | jq -r '.name')
            orders=$(get_orders --user_id 1)
            total=$(echo "$orders" | jq '[.[].total] | add')
            echo "User: $name, Total: $total"
        "#;
        let resp = tool
            .execute(ToolRequest {
                commands: script.to_string(),
                timeout_ms: None,
            })
            .await;
        assert_eq!(resp.exit_code, 0);
        assert_eq!(resp.stdout.trim(), "User: Alice, Total: 79.49");
    }

    #[tokio::test]
    async fn test_execute_tool_failure() {
        let tool = build_test_tool();
        let resp = tool
            .execute(ToolRequest {
                commands: "fail_tool".to_string(),
                timeout_ms: None,
            })
            .await;
        assert_ne!(resp.exit_code, 0);
        assert!(resp.stderr.contains("service unavailable"));
    }

    #[tokio::test]
    async fn test_execute_tool_failure_with_fallback() {
        let tool = build_test_tool();
        let resp = tool
            .execute(ToolRequest {
                commands: "fail_tool || echo 'fallback'".to_string(),
                timeout_ms: None,
            })
            .await;
        assert_eq!(resp.exit_code, 0);
        assert!(resp.stdout.contains("fallback"));
    }

    #[tokio::test]
    async fn test_execute_stdin_pipe() {
        let tool = build_test_tool();
        let resp = tool
            .execute(ToolRequest {
                commands: "echo hello | from_stdin".to_string(),
                timeout_ms: None,
            })
            .await;
        assert_eq!(resp.exit_code, 0);
        assert_eq!(resp.stdout.trim(), "HELLO");
    }

    #[tokio::test]
    async fn test_execute_loop_over_tools() {
        let tool = build_test_tool();
        let script = r#"
            for uid in 1 2 3; do
                get_user --id $uid | jq -r '.name'
            done
        "#;
        let resp = tool
            .execute(ToolRequest {
                commands: script.to_string(),
                timeout_ms: None,
            })
            .await;
        assert_eq!(resp.exit_code, 0);
        assert_eq!(resp.stdout.trim(), "Alice\nAlice\nAlice");
    }

    #[tokio::test]
    async fn test_execute_conditional() {
        let tool = build_test_tool();
        let script = r#"
            user=$(get_user --id 5)
            name=$(echo "$user" | jq -r '.name')
            if [ "$name" = "Alice" ]; then
                echo "found alice"
            else
                echo "not alice"
            fi
        "#;
        let resp = tool
            .execute(ToolRequest {
                commands: script.to_string(),
                timeout_ms: None,
            })
            .await;
        assert_eq!(resp.exit_code, 0);
        assert_eq!(resp.stdout.trim(), "found alice");
    }

    #[tokio::test]
    async fn test_execute_with_env() {
        let tool = ScriptedTool::builder("env_test")
            .env("API_BASE", "https://api.example.com")
            .tool(ToolDef::new("noop", "No-op"), |_args: &ToolArgs| {
                Ok("ok\n".to_string())
            })
            .build();

        let resp = tool
            .execute(ToolRequest {
                commands: "echo $API_BASE".to_string(),
                timeout_ms: None,
            })
            .await;
        assert_eq!(resp.exit_code, 0);
        assert_eq!(resp.stdout.trim(), "https://api.example.com");
    }

    #[tokio::test]
    async fn test_execute_with_status_callback() {
        use std::sync::{Arc, Mutex};

        let tool = build_test_tool();
        let phases = Arc::new(Mutex::new(Vec::new()));
        let phases_clone = phases.clone();

        let resp = tool
            .execute_with_status(
                ToolRequest {
                    commands: "get_user --id 1".to_string(),
                    timeout_ms: None,
                },
                Box::new(move |status| {
                    phases_clone
                        .lock()
                        .expect("lock poisoned")
                        .push(status.phase.clone());
                }),
            )
            .await;

        assert_eq!(resp.exit_code, 0);
        let phases = phases.lock().expect("lock poisoned");
        assert!(phases.contains(&"validate".to_string()));
        assert!(phases.contains(&"execute".to_string()));
        assert!(phases.contains(&"complete".to_string()));
    }

    #[tokio::test]
    async fn test_multiple_execute_calls() {
        let tool = build_test_tool();

        let resp1 = tool
            .execute(ToolRequest {
                commands: "get_user --id 1 | jq -r '.name'".to_string(),
                timeout_ms: None,
            })
            .await;
        assert_eq!(resp1.stdout.trim(), "Alice");

        let resp2 = tool
            .execute(ToolRequest {
                commands: "get_orders --user_id 1 | jq 'length'".to_string(),
                timeout_ms: None,
            })
            .await;
        assert_eq!(resp2.stdout.trim(), "2");
    }

    #[tokio::test]
    async fn test_boolean_flag() {
        let tool = ScriptedTool::builder("bool_test")
            .tool(
                ToolDef::new("search", "Search").with_schema(serde_json::json!({
                    "type": "object",
                    "properties": {
                        "query": {"type": "string"},
                        "verbose": {"type": "boolean"}
                    }
                })),
                |args: &ToolArgs| {
                    let q = args.param_str("query").unwrap_or("");
                    let v = args.param_bool("verbose").unwrap_or(false);
                    Ok(format!("q={q} verbose={v}\n"))
                },
            )
            .build();

        let resp = tool
            .execute(ToolRequest {
                commands: "search --verbose --query hello".to_string(),
                timeout_ms: None,
            })
            .await;
        assert_eq!(resp.exit_code, 0);
        assert_eq!(resp.stdout.trim(), "q=hello verbose=true");
    }

    #[tokio::test]
    async fn test_no_schema_treats_as_strings() {
        let tool = ScriptedTool::builder("str_test")
            .tool(
                ToolDef::new("echo_args", "Echo params as JSON"),
                |args: &ToolArgs| Ok(format!("{}\n", args.params)),
            )
            .build();

        let resp = tool
            .execute(ToolRequest {
                commands: "echo_args --name Alice --count 3".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("stdout should be valid JSON");
        assert_eq!(parsed["name"], "Alice");
        assert_eq!(parsed["count"], "3"); // string, not int — no schema
    }

    // -- Shared context tests (#522) --

    #[tokio::test]
    async fn test_shared_arc_across_callbacks() {
        use std::sync::{Arc, Mutex};

        let shared = Arc::new("shared-token".to_string());
        let call_log = Arc::new(Mutex::new(Vec::<String>::new()));

        let s1 = shared.clone();
        let log1 = call_log.clone();
        let s2 = shared.clone();
        let log2 = call_log.clone();

        let tool = ScriptedTool::builder("ctx_test")
            .tool(
                ToolDef::new("tool_a", "First tool"),
                move |_args: &ToolArgs| {
                    log1.lock().expect("lock").push(format!("a:{}", *s1));
                    Ok("a\n".to_string())
                },
            )
            .tool(
                ToolDef::new("tool_b", "Second tool"),
                move |_args: &ToolArgs| {
                    log2.lock().expect("lock").push(format!("b:{}", *s2));
                    Ok("b\n".to_string())
                },
            )
            .build();

        let resp = tool
            .execute(ToolRequest {
                commands: "tool_a && tool_b".to_string(),
                timeout_ms: None,
            })
            .await;
        assert_eq!(resp.exit_code, 0);
        let log = call_log.lock().expect("lock");
        assert_eq!(*log, vec!["a:shared-token", "b:shared-token"]);
    }

    #[tokio::test]
    async fn test_mutable_shared_state_across_callbacks() {
        use std::sync::{Arc, Mutex};

        let counter = Arc::new(Mutex::new(0u64));
        let c = counter.clone();

        let tool = ScriptedTool::builder("mut_test")
            .tool(
                ToolDef::new("increment", "Bump counter"),
                move |_args: &ToolArgs| {
                    let mut count = c.lock().expect("lock");
                    *count += 1;
                    Ok(format!("{count}\n"))
                },
            )
            .build();

        let resp = tool
            .execute(ToolRequest {
                commands: "increment; increment; increment".to_string(),
                timeout_ms: None,
            })
            .await;
        assert_eq!(resp.exit_code, 0);
        assert_eq!(*counter.lock().expect("lock"), 3);
    }

    // -- Fresh interpreter isolation test (#524) --

    #[tokio::test]
    async fn test_fresh_interpreter_per_execute() {
        let tool = ScriptedTool::builder("isolation_test")
            .tool(ToolDef::new("noop", "No-op"), |_args: &ToolArgs| {
                Ok("ok\n".to_string())
            })
            .build();

        // Set a variable in call 1
        let resp1 = tool
            .execute(ToolRequest {
                commands: "export MY_VAR=hello; echo $MY_VAR".to_string(),
                timeout_ms: None,
            })
            .await;
        assert_eq!(resp1.stdout.trim(), "hello");

        // Variable should NOT persist to call 2
        let resp2 = tool
            .execute(ToolRequest {
                commands: "echo \">${MY_VAR}<\"".to_string(),
                timeout_ms: None,
            })
            .await;
        assert_eq!(resp2.stdout.trim(), "><");
    }

    #[tokio::test]
    async fn test_arc_callback_persists_across_execute_calls() {
        use std::sync::{Arc, Mutex};

        let counter = Arc::new(Mutex::new(0u64));
        let c = counter.clone();

        let tool = ScriptedTool::builder("persist_test")
            .tool(
                ToolDef::new("count", "Count calls"),
                move |_args: &ToolArgs| {
                    let mut n = c.lock().expect("lock");
                    *n += 1;
                    Ok(format!("{n}\n"))
                },
            )
            .build();

        // Call 1
        let resp1 = tool
            .execute(ToolRequest {
                commands: "count".to_string(),
                timeout_ms: None,
            })
            .await;
        assert_eq!(resp1.stdout.trim(), "1");

        // Call 2 — counter persists via Arc
        let resp2 = tool
            .execute(ToolRequest {
                commands: "count".to_string(),
                timeout_ms: None,
            })
            .await;
        assert_eq!(resp2.stdout.trim(), "2");
    }

    #[tokio::test]
    async fn test_execution_trace_records_help_discover_and_tool_invocations() {
        let tool = build_test_tool();

        let resp = tool
            .execute(ToolRequest {
                commands: "discover --search user\nhelp get_user\nget_user --id 42".to_string(),
                timeout_ms: None,
            })
            .await;
        assert_eq!(resp.exit_code, 0);

        let trace = tool
            .take_last_execution_trace()
            .expect("execution trace should be recorded");
        assert_eq!(trace.invocations.len(), 3);
        assert_eq!(trace.invocations[0].name, "discover");
        assert_eq!(trace.invocations[0].kind, ScriptedCommandKind::Discover);
        assert_eq!(trace.invocations[1].name, "help");
        assert_eq!(trace.invocations[1].kind, ScriptedCommandKind::Help);
        assert_eq!(trace.invocations[2].name, "get_user");
        assert_eq!(trace.invocations[2].kind, ScriptedCommandKind::Tool);
    }
}
