//! Agentic Workflow Engine — Phase-87
//!
//! Implements a ReAct (Reasoning + Acting) loop Agent that orchestrates
//! LLM Gateway + RAG + Function Calling + SQL + Graph capabilities.
//!
//! # Architecture
//!
//! ```text
//! ┌─────────────────────────────────────────────────────────────┐
//! │                       AgentEngine                           │
//! │  ┌──────────┐  ┌──────────────┐  ┌───────────────────────┐ │
//! │  │ AgentPlan │  │ ToolRegistry │  │ LlmClient (chat)      │ │
//! │  │  (steps)  │  │  (executors) │  │  (reasoning engine)   │ │
//! │  └────┬─────┘  └──────┬───────┘  └─────────┬─────────────┘ │
//! │       │               │                    │               │
//! │       ▼               ▼                    ▼               │
//! │  ┌──────────────────────────────────────────────────────┐  │
//! │  │            ReAct Loop (max_steps)                     │  │
//! │  │  1. Thought  — LLM reasons about the task            │  │
//! │  │  2. Action   — LLM decides which tool to call        │  │
//! │  │  3. Observe  — Execute tool, get result              │  │
//! │  │  4. Repeat or emit Final Answer                      │  │
//! │  └──────────────────────────────────────────────────────┘  │
//! │                        │                                   │
//! │              AgentResult (steps + answer)                  │
//! └─────────────────────────────────────────────────────────────┘
//! ```

use std::collections::HashMap;
use std::sync::Arc;
use std::time::Instant;

use serde::{Deserialize, Serialize};
use tracing::{debug, info, warn};

use super::client::{LlmClient, LlmMessage};
use super::error::{LlmError, LlmResult};
use super::tool_call_loop::ToolCallLoop;
use super::tool_executor::{ToolRegistry, ToolResult};
use super::types::{ChatConfig, FinishReason, TokenUsage, ToolCall};

// ============================================================================
// AgentConfig — Configuration for the Agent
// ============================================================================

/// Configuration for the Agent engine.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentConfig {
    /// Maximum number of ReAct steps before forcing a final answer.
    pub max_steps: usize,

    /// Maximum number of tool calls per step.
    pub max_tool_calls_per_step: usize,

    /// Maximum total token budget for the entire agent session.
    pub max_total_tokens: usize,

    /// Maximum execution time in seconds.
    pub max_execution_time_secs: u64,

    /// LLM temperature for reasoning.
    pub temperature: f32,

    /// LLM model name (optional, uses default if None).
    pub model: Option<String>,

    /// Whether to enable planning phase before execution.
    pub enable_planning: bool,

    /// Whether the agent runs in read-only mode (no write tools).
    pub readonly_mode: bool,

    /// Whether dangerous operations require confirmation.
    pub confirm_mode: bool,

    /// System prompt for the agent.
    pub system_prompt: Option<String>,

    /// Agent template name (e.g., "data_analyst", "knowledge_assistant", "data_manager").
    pub template: Option<String>,

    /// Allowed tool names (empty = all enabled tools).
    pub allowed_tools: Vec<String>,

    /// Tool call result cache — avoid re-calling same tool with same args.
    pub enable_result_cache: bool,
}

impl Default for AgentConfig {
    fn default() -> Self {
        Self {
            max_steps: 10,
            max_tool_calls_per_step: 5,
            max_total_tokens: 100_000,
            max_execution_time_secs: 120,
            temperature: 0.7,
            model: None,
            enable_planning: true,
            readonly_mode: false,
            confirm_mode: false,
            system_prompt: None,
            template: None,
            allowed_tools: Vec::new(),
            enable_result_cache: true,
        }
    }
}

// ============================================================================
// AgentStep — A single step in the agent's execution
// ============================================================================

/// Type of an agent step.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum AgentStepType {
    /// Initial planning step.
    Plan,
    /// Reasoning / thinking step.
    Thought,
    /// Tool call action step.
    Action,
    /// Observation of tool result.
    Observation,
    /// Final answer.
    Answer,
    /// Error encountered during execution.
    Error,
}

/// A single step in the agent's ReAct execution.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentStep {
    /// Step number (1-based).
    pub step_number: usize,

    /// Type of this step.
    pub step_type: AgentStepType,

    /// Content of the step (thought text, tool call info, observation, or answer).
    pub content: String,

    /// Tool calls made in this step (if Action step).
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub tool_calls: Vec<ToolCall>,

    /// Tool results for this step (if Observation step).
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub tool_results: Vec<AgentToolResult>,

    /// Duration of this step in milliseconds.
    pub duration_ms: u64,

    /// Token usage for this step.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub usage: Option<TokenUsage>,
}

/// Tool result within an agent step.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentToolResult {
    /// Tool call ID.
    pub call_id: String,
    /// Tool name.
    pub tool_name: String,
    /// Whether the tool succeeded.
    pub success: bool,
    /// Output content (truncated for display).
    pub output: String,
    /// Error message if failed.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub error: Option<String>,
}

// ============================================================================
// AgentPlan — Multi-step plan generated by the planner
// ============================================================================

/// A plan generated by the AgentPlanner.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentPlan {
    /// Goal description.
    pub goal: String,

    /// Planned steps.
    pub steps: Vec<PlanStep>,
}

/// A single step in an agent plan.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlanStep {
    /// Step ID (e.g., "step-1").
    pub step_id: String,

    /// Description of what this step does.
    pub description: String,

    /// Tools that might be needed for this step.
    #[serde(default)]
    pub required_tools: Vec<String>,

    /// Dependencies on other step IDs.
    #[serde(default)]
    pub depends_on: Vec<String>,

    /// Whether this step has been completed.
    #[serde(default)]
    pub completed: bool,

    /// Output from this step (filled during execution).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub output: Option<String>,
}

// ============================================================================
// AgentResult — Final output of the agent execution
// ============================================================================

/// The final result of an agent execution.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentResult {
    /// The final answer from the agent.
    pub answer: String,

    /// All steps executed during the agent's reasoning.
    pub steps: Vec<AgentStep>,

    /// The plan (if planning was enabled).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub plan: Option<AgentPlan>,

    /// Total number of tool calls executed.
    pub total_tool_calls: usize,

    /// Total number of ReAct steps.
    pub total_steps: usize,

    /// Aggregated token usage.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub total_usage: Option<TokenUsage>,

    /// Total wall-clock execution time in milliseconds.
    pub total_duration_ms: u64,

    /// Whether the agent completed successfully or was truncated.
    pub completed: bool,

    /// Error message if the agent failed.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub error: Option<String>,

    /// The model used.
    pub model: String,
}

// ============================================================================
// AgentContext — Execution context with result caching
// ============================================================================

/// Execution context for the agent, including tool result caching.
#[derive(Debug, Clone, Default)]
pub struct AgentContext {
    /// Cached tool call results (key: "tool_name:arguments_hash").
    tool_cache: HashMap<String, ToolResult>,

    /// Intermediate data passed between steps.
    pub variables: HashMap<String, serde_json::Value>,

    /// Current step number.
    pub current_step: usize,

    /// Total tokens consumed so far.
    pub tokens_used: usize,
}

impl AgentContext {
    pub fn new() -> Self {
        Self::default()
    }

    /// Get a cached tool result.
    pub fn get_cached(&self, tool_name: &str, arguments: &str) -> Option<&ToolResult> {
        let key = format!("{}:{}", tool_name, Self::hash_args(arguments));
        self.tool_cache.get(&key)
    }

    /// Cache a tool result.
    pub fn cache_result(&mut self, tool_name: &str, arguments: &str, result: ToolResult) {
        let key = format!("{}:{}", tool_name, Self::hash_args(arguments));
        self.tool_cache.insert(key, result);
    }

    /// Simple argument hash for cache key.
    fn hash_args(args: &str) -> u64 {
        use std::hash::{Hash, Hasher};
        let mut hasher = std::collections::hash_map::DefaultHasher::new();
        args.hash(&mut hasher);
        hasher.finish()
    }
}

// ============================================================================
// AgentEngine — The ReAct loop engine
// ============================================================================

/// The Agent Engine implementing the ReAct (Reasoning + Acting) loop.
///
/// Orchestrates LLM reasoning with tool execution to accomplish complex tasks.
///
/// # Example
///
/// ```rust,ignore
/// let engine = AgentEngine::new(client, registry);
/// let config = AgentConfig::default();
/// let result = engine.run("Analyze the sales data and give me a summary", &config).await?;
/// println!("Answer: {}", result.answer);
/// println!("Steps taken: {}", result.total_steps);
/// ```
pub struct AgentEngine {
    /// The LLM client for reasoning.
    client: Arc<dyn LlmClient>,

    /// The tool registry for executing actions.
    registry: Arc<ToolRegistry>,
}

impl AgentEngine {
    /// Create a new AgentEngine.
    pub fn new(client: Arc<dyn LlmClient>, registry: Arc<ToolRegistry>) -> Self {
        Self { client, registry }
    }

    /// Run the agent with a user query.
    ///
    /// This is the main entry point for agent execution:
    /// 1. (Optional) Generate a plan
    /// 2. Execute the ReAct loop
    /// 3. Return the final answer with execution trace
    pub async fn run(&self, query: &str, config: &AgentConfig) -> LlmResult<AgentResult> {
        let start = Instant::now();
        let mut context = AgentContext::new();
        let mut steps: Vec<AgentStep> = Vec::new();
        let mut total_usage: Option<TokenUsage> = None;

        // Build the system prompt
        let system_prompt = self.build_system_prompt(config);
        let tool_definitions = self.get_tool_definitions(config);

        // Phase 1: Planning (optional)
        let plan = if config.enable_planning {
            let plan_start = Instant::now();
            match self.generate_plan(query, &system_prompt, config).await {
                Ok(plan) => {
                    steps.push(AgentStep {
                        step_number: 1,
                        step_type: AgentStepType::Plan,
                        content: format!(
                            "Plan: {} steps — {}",
                            plan.steps.len(),
                            plan.steps
                                .iter()
                                .map(|s| s.description.as_str())
                                .collect::<Vec<_>>()
                                .join("; ")
                        ),
                        tool_calls: Vec::new(),
                        tool_results: Vec::new(),
                        duration_ms: plan_start.elapsed().as_millis() as u64,
                        usage: None,
                    });
                    Some(plan)
                }
                Err(e) => {
                    warn!("Planning failed, proceeding without plan: {}", e);
                    None
                }
            }
        } else {
            None
        };

        // Phase 2: ReAct loop
        let mut conversation: Vec<LlmMessage> = Vec::new();
        conversation.push(LlmMessage::system(&system_prompt));
        conversation.push(LlmMessage::user(query));

        // If we have a plan, include it in context
        if let Some(ref plan) = plan {
            let plan_context = format!(
                "\n[Plan]\n{}\n\nFollow this plan step by step. After completing each step, report the observation and move to the next.",
                plan.steps
                    .iter()
                    .enumerate()
                    .map(|(i, s)| format!("{}. {}", i + 1, s.description))
                    .collect::<Vec<_>>()
                    .join("\n")
            );
            conversation.push(LlmMessage::system(&plan_context));
        }

        let chat_config = ChatConfig {
            temperature: Some(config.temperature),
            max_tokens: Some(4096),
            tools: tool_definitions,
            ..Default::default()
        };

        let mut step_number = steps.len() + 1;
        let total_tool_calls;

        // Use the existing ToolCallLoop for the heavy lifting
        let tool_loop = ToolCallLoop::new(
            Arc::clone(&self.client),
            Arc::clone(&self.registry),
        );

        // Check time budget
        let time_limit = std::time::Duration::from_secs(config.max_execution_time_secs);

        // Execute: We use the ToolCallLoop's run() which already handles
        // the multi-round tool calling. But we add ReAct-style step tracking.
        let loop_result = tokio::select! {
            result = tool_loop.run(conversation.clone(), &chat_config) => {
                result?
            }
            _ = tokio::time::sleep(time_limit) => {
                return Ok(AgentResult {
                    answer: "Agent execution timed out. Please try a simpler query or increase the time limit.".to_string(),
                    steps,
                    plan,
                    total_tool_calls: 0,
                    total_steps: step_number - 1,
                    total_usage,
                    total_duration_ms: start.elapsed().as_millis() as u64,
                    completed: false,
                    error: Some("Execution timed out".to_string()),
                    model: config.model.clone().unwrap_or_else(|| "default".to_string()),
                });
            }
        };

        // Convert ToolCallLoop rounds into AgentSteps
        for round in &loop_result.rounds {
            // Action step (tool calls)
            let tool_names: Vec<String> = round.tool_calls.iter().map(|tc| tc.name.clone()).collect();
            steps.push(AgentStep {
                step_number,
                step_type: AgentStepType::Action,
                content: format!("Calling tools: {}", tool_names.join(", ")),
                tool_calls: round.tool_calls.clone(),
                tool_results: Vec::new(),
                duration_ms: 0,
                usage: None,
            });
            step_number += 1;

            // Observation step (tool results)
            let agent_results: Vec<AgentToolResult> = round.results.iter().map(|r| {
                AgentToolResult {
                    call_id: r.call_id.clone(),
                    tool_name: r.tool_name.clone(),
                    success: r.result.success,
                    output: truncate_output(&r.result.output, 500),
                    error: r.result.error.clone(),
                }
            }).collect();

            // Cache results in context
            for (tc, tr) in round.tool_calls.iter().zip(round.results.iter()) {
                if config.enable_result_cache {
                    context.cache_result(&tc.name, &tc.arguments, tr.result.clone());
                }
            }

            steps.push(AgentStep {
                step_number,
                step_type: AgentStepType::Observation,
                content: agent_results
                    .iter()
                    .map(|r| {
                        if r.success {
                            format!("[{}] OK: {}", r.tool_name, truncate_output(&r.output, 200))
                        } else {
                            format!("[{}] ERROR: {}", r.tool_name, r.error.as_deref().unwrap_or("unknown"))
                        }
                    })
                    .collect::<Vec<_>>()
                    .join("\n"),
                tool_calls: Vec::new(),
                tool_results: agent_results,
                duration_ms: 0,
                usage: None,
            });
            step_number += 1;
        }

        total_tool_calls = loop_result.total_tool_calls;
        total_usage = loop_result.total_usage.clone();

        // Final answer step
        steps.push(AgentStep {
            step_number,
            step_type: AgentStepType::Answer,
            content: loop_result.content.clone(),
            tool_calls: Vec::new(),
            tool_results: Vec::new(),
            duration_ms: 0,
            usage: None,
        });

        let total_duration_ms = start.elapsed().as_millis() as u64;
        let total_steps = steps.len();

        info!(
            total_steps,
            total_tool_calls,
            total_duration_ms,
            "Agent execution completed"
        );

        Ok(AgentResult {
            answer: loop_result.content,
            steps,
            plan,
            total_tool_calls,
            total_steps,
            total_usage,
            total_duration_ms,
            completed: true,
            error: None,
            model: loop_result.model,
        })
    }

    /// Generate a multi-step plan for a complex query.
    async fn generate_plan(
        &self,
        query: &str,
        system_prompt: &str,
        config: &AgentConfig,
    ) -> LlmResult<AgentPlan> {
        let plan_prompt = format!(
            r#"You are a planning assistant. Given the user's request, create a step-by-step plan.

Available tools: {}

User request: {}

Respond with a JSON plan:
{{
  "goal": "brief goal description",
  "steps": [
    {{
      "step_id": "step-1",
      "description": "what to do",
      "required_tools": ["tool_name"],
      "depends_on": []
    }}
  ]
}}

Keep the plan concise (2-5 steps). Only output JSON, no other text."#,
            self.get_available_tool_names(config).join(", "),
            query
        );

        let messages = vec![
            LlmMessage::system(system_prompt),
            LlmMessage::user(&plan_prompt),
        ];

        let plan_config = ChatConfig {
            temperature: Some(0.3),
            max_tokens: Some(1024),
            ..Default::default()
        };

        let response = self.client.chat(&messages, &plan_config).await?;

        // Try to parse the plan from JSON
        let plan_text = extract_json_from_response(&response.content);
        match serde_json::from_str::<AgentPlan>(&plan_text) {
            Ok(plan) => {
                info!(steps = plan.steps.len(), "Agent plan generated");
                Ok(plan)
            }
            Err(e) => {
                debug!("Failed to parse plan JSON: {} — raw: {}", e, response.content);
                // Create a simple single-step plan as fallback
                Ok(AgentPlan {
                    goal: query.to_string(),
                    steps: vec![PlanStep {
                        step_id: "step-1".to_string(),
                        description: format!("Answer: {}", query),
                        required_tools: Vec::new(),
                        depends_on: Vec::new(),
                        completed: false,
                        output: None,
                    }],
                })
            }
        }
    }

    /// Build the system prompt for the agent.
    fn build_system_prompt(&self, config: &AgentConfig) -> String {
        let template_prompt = match config.template.as_deref() {
            Some("data_analyst") => DATA_ANALYST_PROMPT,
            Some("knowledge_assistant") => KNOWLEDGE_ASSISTANT_PROMPT,
            Some("data_manager") => DATA_MANAGER_PROMPT,
            _ => DEFAULT_AGENT_PROMPT,
        };

        let custom = config
            .system_prompt
            .as_deref()
            .unwrap_or("");

        let mode_note = if config.readonly_mode {
            "\n\nIMPORTANT: You are in READ-ONLY mode. Do NOT call any tools that modify data."
        } else if config.confirm_mode {
            "\n\nIMPORTANT: Before executing any data-modifying operation, explain what you plan to do and ask for confirmation."
        } else {
            ""
        };

        let tool_list = self.get_available_tool_names(config);
        let tool_section = if tool_list.is_empty() {
            String::new()
        } else {
            format!("\n\nAvailable tools: {}", tool_list.join(", "))
        };

        format!("{}{}{}{}", template_prompt, custom, mode_note, tool_section)
    }

    /// Get tool definitions filtered by config.
    fn get_tool_definitions(&self, config: &AgentConfig) -> Vec<super::types::ToolDefinition> {
        if config.readonly_mode {
            // Only read-only tools
            self.registry
                .list_read_only_tools()
                .iter()
                .filter(|m| {
                    config.allowed_tools.is_empty()
                        || config.allowed_tools.contains(&m.name)
                })
                .map(|m| m.to_tool_definition())
                .collect()
        } else if config.allowed_tools.is_empty() {
            self.registry.tool_definitions()
        } else {
            let names: Vec<&str> = config.allowed_tools.iter().map(|s| s.as_str()).collect();
            self.registry.tool_definitions_for(&names)
        }
    }

    /// Get available tool names filtered by config.
    fn get_available_tool_names(&self, config: &AgentConfig) -> Vec<String> {
        if config.readonly_mode {
            self.registry
                .list_read_only_tools()
                .iter()
                .filter(|m| {
                    config.allowed_tools.is_empty()
                        || config.allowed_tools.contains(&m.name)
                })
                .map(|m| m.name.clone())
                .collect()
        } else if config.allowed_tools.is_empty() {
            self.registry.list_tool_names()
        } else {
            config.allowed_tools.clone()
        }
    }
}

// ============================================================================
// Agent Template System Prompts
// ============================================================================

const DEFAULT_AGENT_PROMPT: &str = r#"You are an AI assistant with access to database tools. You can search data, query databases, analyze information, and help users understand their data.

When given a task:
1. Think about what information you need
2. Use the available tools to gather information
3. Analyze the results
4. Provide a clear, comprehensive answer

Always explain your reasoning. If a tool call fails, try an alternative approach."#;

const DATA_ANALYST_PROMPT: &str = r#"You are a Data Analyst AI agent. Your role is to help users understand and analyze their data.

Capabilities:
- Convert natural language questions to SQL queries
- Execute SQL queries to retrieve and aggregate data
- Generate statistical summaries and insights
- Identify trends and patterns in data

When analyzing data:
1. First understand the data schema (use schema exploration tools)
2. Write and execute appropriate SQL queries
3. Interpret the results
4. Present findings in a clear, actionable format

Always show your SQL queries and explain the results. If a query fails, debug and retry with corrections."#;

const KNOWLEDGE_ASSISTANT_PROMPT: &str = r#"You are a Knowledge Assistant AI agent. Your role is to help users find and understand information from their knowledge base.

Capabilities:
- Search documents using semantic similarity (vector search)
- Search documents using keyword matching (full-text search)
- Explore knowledge graphs for entity relationships
- Synthesize information from multiple sources

When answering questions:
1. Search the knowledge base using appropriate retrieval methods
2. If the initial results aren't sufficient, try different search strategies
3. Cross-reference information from multiple sources
4. Cite your sources and explain confidence levels

Always provide source references. Distinguish between information you found in the knowledge base and your own reasoning."#;

const DATA_MANAGER_PROMPT: &str = r#"You are a Data Manager AI agent. Your role is to help users manage their data collections.

Capabilities:
- Explore database schemas and collections
- Create, read, update, and delete documents
- Batch import data
- Manage collection configurations

When managing data:
1. Always confirm destructive operations (delete, update) before executing
2. Validate data before inserting
3. Report what changes were made
4. Suggest optimizations when appropriate

Be careful with write operations. Always explain what you're about to do before doing it."#;

// ============================================================================
// AgentTemplate — Pre-configured agent templates
// ============================================================================

/// Pre-defined agent template with configured tools and system prompt.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentTemplate {
    /// Template identifier.
    pub id: String,
    /// Display name.
    pub name: String,
    /// Description of what this agent does.
    pub description: String,
    /// Pre-configured tools for this template.
    pub tools: Vec<String>,
    /// System prompt for this template.
    pub system_prompt: String,
    /// Default configuration overrides.
    pub config: AgentTemplateConfig,
}

/// Default config overrides for a template.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentTemplateConfig {
    pub max_steps: Option<usize>,
    pub temperature: Option<f32>,
    pub readonly_mode: Option<bool>,
    pub enable_planning: Option<bool>,
}

impl Default for AgentTemplateConfig {
    fn default() -> Self {
        Self {
            max_steps: None,
            temperature: None,
            readonly_mode: None,
            enable_planning: None,
        }
    }
}

/// Get a built-in agent template by ID.
pub fn get_builtin_template(id: &str) -> Option<AgentTemplate> {
    match id {
        "data_analyst" => Some(AgentTemplate {
            id: "data_analyst".to_string(),
            name: "Data Analyst".to_string(),
            description: "Analyzes data using SQL queries, generates statistical summaries and insights.".to_string(),
            tools: vec![
                "db_sql_query".to_string(),
                "db_vector_search".to_string(),
                "db_list_sources".to_string(),
            ],
            system_prompt: DATA_ANALYST_PROMPT.to_string(),
            config: AgentTemplateConfig {
                max_steps: Some(15),
                temperature: Some(0.3),
                readonly_mode: Some(true),
                enable_planning: Some(true),
            },
        }),
        "knowledge_assistant" => Some(AgentTemplate {
            id: "knowledge_assistant".to_string(),
            name: "Knowledge Assistant".to_string(),
            description: "Searches and synthesizes knowledge from documents and knowledge graphs.".to_string(),
            tools: vec![
                "db_vector_search".to_string(),
                "db_fts_search".to_string(),
                "db_graph_query".to_string(),
                "db_list_sources".to_string(),
            ],
            system_prompt: KNOWLEDGE_ASSISTANT_PROMPT.to_string(),
            config: AgentTemplateConfig {
                max_steps: Some(10),
                temperature: Some(0.5),
                readonly_mode: Some(true),
                enable_planning: Some(true),
            },
        }),
        "data_manager" => Some(AgentTemplate {
            id: "data_manager".to_string(),
            name: "Data Manager".to_string(),
            description: "Manages data collections — CRUD, batch import, schema exploration.".to_string(),
            tools: vec![
                "db_doc_crud".to_string(),
                "db_sql_query".to_string(),
                "db_list_sources".to_string(),
                "db_vector_search".to_string(),
            ],
            system_prompt: DATA_MANAGER_PROMPT.to_string(),
            config: AgentTemplateConfig {
                max_steps: Some(10),
                temperature: Some(0.3),
                readonly_mode: Some(false),
                enable_planning: Some(true),
            },
        }),
        _ => None,
    }
}

/// List all built-in agent templates.
pub fn list_builtin_templates() -> Vec<AgentTemplate> {
    vec!["data_analyst", "knowledge_assistant", "data_manager"]
        .into_iter()
        .filter_map(get_builtin_template)
        .collect()
}

/// Apply a template to an AgentConfig.
pub fn apply_template(config: &mut AgentConfig, template: &AgentTemplate) {
    config.template = Some(template.id.clone());
    config.system_prompt = Some(template.system_prompt.clone());
    config.allowed_tools = template.tools.clone();

    if let Some(max_steps) = template.config.max_steps {
        config.max_steps = max_steps;
    }
    if let Some(temp) = template.config.temperature {
        config.temperature = temp;
    }
    if let Some(readonly) = template.config.readonly_mode {
        config.readonly_mode = readonly;
    }
    if let Some(planning) = template.config.enable_planning {
        config.enable_planning = planning;
    }
}

// ============================================================================
// Security Sandbox — Permission control + Resource limits
// ============================================================================

/// Resource usage tracker for agent sandboxing.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentResourceUsage {
    /// Total tool calls made.
    pub tool_calls: usize,
    /// Total tokens consumed.
    pub tokens: usize,
    /// Execution time in milliseconds.
    pub execution_time_ms: u64,
    /// Number of write operations attempted.
    pub write_operations: usize,
}

impl Default for AgentResourceUsage {
    fn default() -> Self {
        Self {
            tool_calls: 0,
            tokens: 0,
            execution_time_ms: 0,
            write_operations: 0,
        }
    }
}

/// Resource limits for agent execution.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentResourceLimits {
    /// Maximum tool calls per session.
    pub max_tool_calls: usize,
    /// Maximum tokens per session.
    pub max_tokens: usize,
    /// Maximum execution time in seconds.
    pub max_execution_time_secs: u64,
    /// Maximum write operations (0 = read-only).
    pub max_write_operations: usize,
}

impl Default for AgentResourceLimits {
    fn default() -> Self {
        Self {
            max_tool_calls: 50,
            max_tokens: 100_000,
            max_execution_time_secs: 300,
            max_write_operations: 10,
        }
    }
}

impl AgentResourceLimits {
    /// Check if a resource limit has been exceeded.
    pub fn check(&self, usage: &AgentResourceUsage) -> Option<String> {
        if usage.tool_calls >= self.max_tool_calls {
            return Some(format!(
                "Tool call limit reached ({}/{})",
                usage.tool_calls, self.max_tool_calls
            ));
        }
        if usage.tokens >= self.max_tokens {
            return Some(format!(
                "Token limit reached ({}/{})",
                usage.tokens, self.max_tokens
            ));
        }
        if usage.execution_time_ms >= self.max_execution_time_secs * 1000 {
            return Some(format!(
                "Execution time limit reached ({}s/{}s)",
                usage.execution_time_ms / 1000,
                self.max_execution_time_secs
            ));
        }
        if self.max_write_operations == 0 && usage.write_operations > 0 {
            return Some("Write operations are not allowed (read-only mode)".to_string());
        }
        if self.max_write_operations > 0 && usage.write_operations >= self.max_write_operations {
            return Some(format!(
                "Write operation limit reached ({}/{})",
                usage.write_operations, self.max_write_operations
            ));
        }
        None
    }

    /// Create read-only limits.
    pub fn read_only() -> Self {
        Self {
            max_write_operations: 0,
            ..Default::default()
        }
    }
}

// ============================================================================
// Helpers
// ============================================================================

/// Truncate output for display, preserving useful content.
fn truncate_output(s: &str, max_len: usize) -> String {
    if s.len() <= max_len {
        s.to_string()
    } else {
        format!("{}... (truncated, {} total chars)", &s[..max_len], s.len())
    }
}

/// Extract JSON from an LLM response (handles markdown code fences).
fn extract_json_from_response(response: &str) -> String {
    let trimmed = response.trim();

    // Check for ```json ... ``` pattern
    if let Some(start) = trimmed.find("```json") {
        let json_start = start + 7;
        if let Some(end) = trimmed[json_start..].find("```") {
            return trimmed[json_start..json_start + end].trim().to_string();
        }
    }

    // Check for ``` ... ``` pattern
    if let Some(start) = trimmed.find("```") {
        let inner_start = start + 3;
        // Skip language identifier on the first line
        let after_lang = if let Some(nl) = trimmed[inner_start..].find('\n') {
            inner_start + nl + 1
        } else {
            inner_start
        };
        if let Some(end) = trimmed[after_lang..].find("```") {
            return trimmed[after_lang..after_lang + end].trim().to_string();
        }
    }

    // Check if it starts with { — assume raw JSON
    if trimmed.starts_with('{') {
        return trimmed.to_string();
    }

    trimmed.to_string()
}

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

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

    #[test]
    fn test_agent_config_default() {
        let config = AgentConfig::default();
        assert_eq!(config.max_steps, 10);
        assert_eq!(config.max_tool_calls_per_step, 5);
        assert!(!config.readonly_mode);
        assert!(config.enable_planning);
        assert!(config.enable_result_cache);
        assert!(config.allowed_tools.is_empty());
    }

    #[test]
    fn test_agent_context_caching() {
        let mut ctx = AgentContext::new();
        let result = ToolResult::ok("test output");

        ctx.cache_result("echo", r#"{"msg":"hello"}"#, result.clone());

        let cached = ctx.get_cached("echo", r#"{"msg":"hello"}"#);
        assert!(cached.is_some());
        assert_eq!(cached.unwrap().output, "test output");

        // Different args should not be cached
        let not_cached = ctx.get_cached("echo", r#"{"msg":"world"}"#);
        assert!(not_cached.is_none());
    }

    #[test]
    fn test_extract_json_from_response_raw() {
        let raw = r#"{"goal": "test", "steps": []}"#;
        assert_eq!(extract_json_from_response(raw), raw);
    }

    #[test]
    fn test_extract_json_from_response_fenced() {
        let fenced = "```json\n{\"goal\": \"test\"}\n```";
        assert_eq!(extract_json_from_response(fenced), r#"{"goal": "test"}"#);
    }

    #[test]
    fn test_extract_json_from_response_code_block() {
        let block = "Here is the plan:\n```\n{\"goal\": \"test\"}\n```\nDone.";
        assert_eq!(extract_json_from_response(block), r#"{"goal": "test"}"#);
    }

    #[test]
    fn test_truncate_output() {
        let short = "hello";
        assert_eq!(truncate_output(short, 10), "hello");

        let long = "a".repeat(100);
        let truncated = truncate_output(&long, 20);
        assert!(truncated.contains("truncated"));
        assert!(truncated.len() < 100);
    }

    #[test]
    fn test_builtin_templates() {
        let templates = list_builtin_templates();
        assert_eq!(templates.len(), 3);

        let da = get_builtin_template("data_analyst").unwrap();
        assert_eq!(da.id, "data_analyst");
        assert!(da.config.readonly_mode.unwrap());

        let ka = get_builtin_template("knowledge_assistant").unwrap();
        assert_eq!(ka.id, "knowledge_assistant");
        assert!(ka.tools.contains(&"db_vector_search".to_string()));

        let dm = get_builtin_template("data_manager").unwrap();
        assert_eq!(dm.id, "data_manager");
        assert!(!dm.config.readonly_mode.unwrap());
    }

    #[test]
    fn test_apply_template() {
        let template = get_builtin_template("data_analyst").unwrap();
        let mut config = AgentConfig::default();
        apply_template(&mut config, &template);

        assert_eq!(config.max_steps, 15);
        assert_eq!(config.temperature, 0.3);
        assert!(config.readonly_mode);
        assert!(config.enable_planning);
        assert_eq!(config.allowed_tools, template.tools);
    }

    #[test]
    fn test_resource_limits_check() {
        let limits = AgentResourceLimits {
            max_tool_calls: 5,
            max_tokens: 1000,
            max_execution_time_secs: 60,
            max_write_operations: 2,
        };

        // Under limits
        let usage = AgentResourceUsage {
            tool_calls: 3,
            tokens: 500,
            execution_time_ms: 30000,
            write_operations: 1,
        };
        assert!(limits.check(&usage).is_none());

        // Tool call limit exceeded
        let usage_tc = AgentResourceUsage {
            tool_calls: 5,
            ..Default::default()
        };
        let msg = limits.check(&usage_tc).unwrap();
        assert!(msg.contains("Tool call limit"));

        // Token limit exceeded
        let usage_tok = AgentResourceUsage {
            tokens: 1000,
            ..Default::default()
        };
        let msg = limits.check(&usage_tok).unwrap();
        assert!(msg.contains("Token limit"));

        // Write limit exceeded
        let usage_w = AgentResourceUsage {
            write_operations: 2,
            ..Default::default()
        };
        let msg = limits.check(&usage_w).unwrap();
        assert!(msg.contains("Write operation limit"));
    }

    #[test]
    fn test_resource_limits_read_only() {
        let limits = AgentResourceLimits::read_only();
        assert_eq!(limits.max_write_operations, 0);

        // Zero writes should be fine
        let usage = AgentResourceUsage::default();
        assert!(limits.check(&usage).is_none());

        // Any write should fail in read-only mode
        let usage_with_write = AgentResourceUsage {
            write_operations: 1,
            ..Default::default()
        };
        let msg = limits.check(&usage_with_write).unwrap();
        assert!(msg.contains("read-only"));
    }

    #[test]
    fn test_agent_step_types() {
        let step = AgentStep {
            step_number: 1,
            step_type: AgentStepType::Thought,
            content: "I need to search the database".to_string(),
            tool_calls: Vec::new(),
            tool_results: Vec::new(),
            duration_ms: 50,
            usage: None,
        };
        assert_eq!(step.step_type, AgentStepType::Thought);
        assert_eq!(step.step_number, 1);
    }

    #[test]
    fn test_agent_plan_serialization() {
        let plan = AgentPlan {
            goal: "Analyze sales data".to_string(),
            steps: vec![
                PlanStep {
                    step_id: "step-1".to_string(),
                    description: "Get schema".to_string(),
                    required_tools: vec!["db_list_sources".to_string()],
                    depends_on: Vec::new(),
                    completed: false,
                    output: None,
                },
                PlanStep {
                    step_id: "step-2".to_string(),
                    description: "Query data".to_string(),
                    required_tools: vec!["db_sql_query".to_string()],
                    depends_on: vec!["step-1".to_string()],
                    completed: false,
                    output: None,
                },
            ],
        };

        let json = serde_json::to_string(&plan).unwrap();
        let parsed: AgentPlan = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed.steps.len(), 2);
        assert_eq!(parsed.steps[1].depends_on, vec!["step-1".to_string()]);
    }

    #[test]
    fn test_agent_result_structure() {
        let result = AgentResult {
            answer: "The total sales are $1M".to_string(),
            steps: vec![
                AgentStep {
                    step_number: 1,
                    step_type: AgentStepType::Plan,
                    content: "Plan: 2 steps".to_string(),
                    tool_calls: Vec::new(),
                    tool_results: Vec::new(),
                    duration_ms: 100,
                    usage: None,
                },
                AgentStep {
                    step_number: 2,
                    step_type: AgentStepType::Answer,
                    content: "The total sales are $1M".to_string(),
                    tool_calls: Vec::new(),
                    tool_results: Vec::new(),
                    duration_ms: 50,
                    usage: None,
                },
            ],
            plan: None,
            total_tool_calls: 0,
            total_steps: 2,
            total_usage: None,
            total_duration_ms: 150,
            completed: true,
            error: None,
            model: "gpt-4".to_string(),
        };
        assert!(result.completed);
        assert_eq!(result.total_steps, 2);
        assert!(result.error.is_none());
    }

    #[test]
    fn test_unknown_template_returns_none() {
        assert!(get_builtin_template("nonexistent").is_none());
    }

    #[test]
    fn test_agent_config_readonly_system_prompt() {
        // Test the build_system_prompt indirectly via config
        let config = AgentConfig {
            readonly_mode: true,
            ..Default::default()
        };
        assert!(config.readonly_mode);
    }

    // ========================================================================
    // Integration Tests — Phase-87 T87.8
    // ========================================================================

    #[test]
    fn test_agent_sse_event_serialization() {
        use super::super::types::StreamEvent;
        use super::super::stream_engine::stream_event_to_sse;

        // AgentPlan event
        let plan_event = StreamEvent::AgentPlan {
            plan: "1. Search data\n2. Analyze results".to_string(),
            step_count: 2,
        };
        let sse = stream_event_to_sse(&plan_event);
        assert_eq!(sse.event, "agent_plan");
        assert!(sse.data.contains("step_count"));
        assert!(sse.data.contains("Search data"));

        // AgentThought event
        let thought_event = StreamEvent::AgentThought {
            step: 1,
            content: "I need to query the database first".to_string(),
        };
        let sse = stream_event_to_sse(&thought_event);
        assert_eq!(sse.event, "agent_thought");
        assert!(sse.data.contains("\"step\":1"));

        // AgentAction event
        let action_event = StreamEvent::AgentAction {
            step: 2,
            tool_name: "db_sql_query".to_string(),
            tool_args: r#"{"sql":"SELECT count(*) FROM users"}"#.to_string(),
        };
        let sse = stream_event_to_sse(&action_event);
        assert_eq!(sse.event, "agent_action");
        assert!(sse.data.contains("db_sql_query"));

        // AgentObservation event
        let obs_event = StreamEvent::AgentObservation {
            step: 2,
            tool_name: "db_sql_query".to_string(),
            success: true,
            output: "42 rows".to_string(),
        };
        let sse = stream_event_to_sse(&obs_event);
        assert_eq!(sse.event, "agent_observation");
        assert!(sse.data.contains("\"success\":true"));
    }

    #[test]
    fn test_agent_sse_event_roundtrip() {
        use super::super::stream_engine::{stream_event_to_sse, parse_sse_event};
        use super::super::types::StreamEvent;

        let events = vec![
            StreamEvent::AgentPlan {
                plan: "step 1, step 2".to_string(),
                step_count: 2,
            },
            StreamEvent::AgentThought {
                step: 1,
                content: "reasoning".to_string(),
            },
            StreamEvent::AgentAction {
                step: 2,
                tool_name: "search".to_string(),
                tool_args: "{}".to_string(),
            },
            StreamEvent::AgentObservation {
                step: 2,
                tool_name: "search".to_string(),
                success: true,
                output: "found it".to_string(),
            },
        ];

        for event in &events {
            let sse = stream_event_to_sse(event);
            let parsed = parse_sse_event(&sse.event, &sse.data).unwrap();
            // Verify roundtrip by re-serializing
            let sse2 = stream_event_to_sse(&parsed);
            assert_eq!(sse.event, sse2.event);
            assert_eq!(sse.data, sse2.data);
        }
    }

    #[test]
    fn test_agent_plan_json_extraction() {
        // Test plan JSON extraction from LLM responses
        let response_with_fenced = r#"Here is the plan:
```json
{
  "goal": "Analyze user data",
  "steps": [
    {
      "step_id": "step-1",
      "description": "List available tables",
      "required_tools": ["db_list_sources"],
      "depends_on": []
    }
  ]
}
```"#;
        let json_str = extract_json_from_response(response_with_fenced);
        let plan: Result<AgentPlan, _> = serde_json::from_str(&json_str);
        assert!(plan.is_ok());
        let plan = plan.unwrap();
        assert_eq!(plan.goal, "Analyze user data");
        assert_eq!(plan.steps.len(), 1);
        assert_eq!(plan.steps[0].step_id, "step-1");
    }

    #[test]
    fn test_agent_tool_result_conversion() {
        let tool_result = ToolResult::ok("42 users found");
        let agent_result = AgentToolResult {
            call_id: "call-1".to_string(),
            tool_name: "db_sql_query".to_string(),
            success: tool_result.success,
            output: tool_result.output.clone(),
            error: tool_result.error.clone(),
        };
        assert!(agent_result.success);
        assert_eq!(agent_result.output, "42 users found");
        assert!(agent_result.error.is_none());

        let err_result = ToolResult::err("Table not found");
        let agent_err = AgentToolResult {
            call_id: "call-2".to_string(),
            tool_name: "db_sql_query".to_string(),
            success: err_result.success,
            output: err_result.output.clone(),
            error: err_result.error.clone(),
        };
        assert!(!agent_err.success);
        assert!(agent_err.error.is_some());
    }

    #[test]
    fn test_agent_config_full_lifecycle() {
        // 1. Create default config
        let mut config = AgentConfig::default();
        assert_eq!(config.max_steps, 10);
        assert!(!config.readonly_mode);

        // 2. Apply template
        let template = get_builtin_template("data_analyst").unwrap();
        apply_template(&mut config, &template);
        assert!(config.readonly_mode);
        assert_eq!(config.max_steps, 15);

        // 3. Override specific values
        config.max_steps = 20;
        config.temperature = 0.5;
        assert_eq!(config.max_steps, 20);
        assert_eq!(config.temperature, 0.5);

        // 4. Serialize and deserialize
        let json = serde_json::to_string(&config).unwrap();
        let restored: AgentConfig = serde_json::from_str(&json).unwrap();
        assert_eq!(restored.max_steps, 20);
        assert_eq!(restored.temperature, 0.5);
        assert!(restored.readonly_mode);
    }

    #[test]
    fn test_agent_context_variable_passing() {
        let mut ctx = AgentContext::new();

        // Set variables
        ctx.variables.insert("user_query".to_string(), serde_json::json!("Show me sales data"));
        ctx.variables.insert("table_name".to_string(), serde_json::json!("sales"));

        // Retrieve variables
        assert_eq!(ctx.variables["user_query"], serde_json::json!("Show me sales data"));
        assert_eq!(ctx.variables["table_name"], serde_json::json!("sales"));

        // Track step progression
        ctx.current_step = 1;
        assert_eq!(ctx.current_step, 1);
        ctx.current_step += 1;
        assert_eq!(ctx.current_step, 2);
    }

    #[test]
    fn test_resource_limits_combined() {
        let limits = AgentResourceLimits {
            max_tool_calls: 10,
            max_tokens: 50_000,
            max_execution_time_secs: 120,
            max_write_operations: 3,
        };

        // All within limits
        let usage = AgentResourceUsage {
            tool_calls: 5,
            tokens: 25_000,
            execution_time_ms: 60_000,
            write_operations: 1,
        };
        assert!(limits.check(&usage).is_none());

        // Multiple near-limit but none exceeding
        let edge_usage = AgentResourceUsage {
            tool_calls: 9,
            tokens: 49_999,
            execution_time_ms: 119_999,
            write_operations: 2,
        };
        assert!(limits.check(&edge_usage).is_none());
    }

    #[test]
    fn test_all_template_configs_valid() {
        // Ensure all templates produce valid configs when applied
        let templates = list_builtin_templates();
        for template in &templates {
            let mut config = AgentConfig::default();
            apply_template(&mut config, template);

            // Config should be serializable
            let json = serde_json::to_string(&config).unwrap();
            let _: AgentConfig = serde_json::from_str(&json).unwrap();

            // System prompt should be set
            assert!(config.system_prompt.is_some());

            // Tools should match template
            assert_eq!(config.allowed_tools, template.tools);
        }
    }

    #[test]
    fn test_agent_result_serialization_complete() {
        let result = AgentResult {
            answer: "Analysis complete: 42 users, $1.2M revenue".to_string(),
            steps: vec![
                AgentStep {
                    step_number: 1,
                    step_type: AgentStepType::Plan,
                    content: "Plan: 3 steps".to_string(),
                    tool_calls: Vec::new(),
                    tool_results: Vec::new(),
                    duration_ms: 200,
                    usage: Some(TokenUsage::new(100, 50)),
                },
                AgentStep {
                    step_number: 2,
                    step_type: AgentStepType::Thought,
                    content: "I need to query user count".to_string(),
                    tool_calls: Vec::new(),
                    tool_results: Vec::new(),
                    duration_ms: 150,
                    usage: None,
                },
                AgentStep {
                    step_number: 3,
                    step_type: AgentStepType::Action,
                    content: "Calling db_sql_query".to_string(),
                    tool_calls: vec![ToolCall {
                        id: "call-1".to_string(),
                        name: "db_sql_query".to_string(),
                        arguments: r#"{"sql":"SELECT count(*) FROM users"}"#.to_string(),
                    }],
                    tool_results: Vec::new(),
                    duration_ms: 50,
                    usage: None,
                },
                AgentStep {
                    step_number: 4,
                    step_type: AgentStepType::Observation,
                    content: "[db_sql_query] OK: 42".to_string(),
                    tool_calls: Vec::new(),
                    tool_results: vec![AgentToolResult {
                        call_id: "call-1".to_string(),
                        tool_name: "db_sql_query".to_string(),
                        success: true,
                        output: "42".to_string(),
                        error: None,
                    }],
                    duration_ms: 300,
                    usage: None,
                },
                AgentStep {
                    step_number: 5,
                    step_type: AgentStepType::Answer,
                    content: "Analysis complete".to_string(),
                    tool_calls: Vec::new(),
                    tool_results: Vec::new(),
                    duration_ms: 100,
                    usage: Some(TokenUsage::new(200, 80)),
                },
            ],
            plan: Some(AgentPlan {
                goal: "Analyze user data and revenue".to_string(),
                steps: vec![
                    PlanStep {
                        step_id: "step-1".to_string(),
                        description: "Count users".to_string(),
                        required_tools: vec!["db_sql_query".to_string()],
                        depends_on: Vec::new(),
                        completed: true,
                        output: Some("42".to_string()),
                    },
                ],
            }),
            total_tool_calls: 1,
            total_steps: 5,
            total_usage: Some(TokenUsage::new(300, 130)),
            total_duration_ms: 800,
            completed: true,
            error: None,
            model: "gpt-4o".to_string(),
        };

        // Full serialization/deserialization roundtrip
        let json = serde_json::to_string_pretty(&result).unwrap();
        let parsed: AgentResult = serde_json::from_str(&json).unwrap();

        assert_eq!(parsed.answer, result.answer);
        assert_eq!(parsed.steps.len(), 5);
        assert_eq!(parsed.total_tool_calls, 1);
        assert_eq!(parsed.total_steps, 5);
        assert!(parsed.completed);
        assert_eq!(parsed.model, "gpt-4o");
        assert!(parsed.plan.is_some());
        let plan = parsed.plan.unwrap();
        assert_eq!(plan.steps.len(), 1);
        assert!(plan.steps[0].completed);

        // Verify step types are preserved
        assert_eq!(parsed.steps[0].step_type, AgentStepType::Plan);
        assert_eq!(parsed.steps[1].step_type, AgentStepType::Thought);
        assert_eq!(parsed.steps[2].step_type, AgentStepType::Action);
        assert_eq!(parsed.steps[3].step_type, AgentStepType::Observation);
        assert_eq!(parsed.steps[4].step_type, AgentStepType::Answer);

        // Verify tool call data preserved
        assert_eq!(parsed.steps[2].tool_calls.len(), 1);
        assert_eq!(parsed.steps[2].tool_calls[0].name, "db_sql_query");
        assert_eq!(parsed.steps[3].tool_results.len(), 1);
        assert!(parsed.steps[3].tool_results[0].success);
    }

    #[test]
    fn test_agent_error_step_in_result() {
        let result = AgentResult {
            answer: "Agent execution timed out.".to_string(),
            steps: vec![
                AgentStep {
                    step_number: 1,
                    step_type: AgentStepType::Error,
                    content: "Execution timed out after 120s".to_string(),
                    tool_calls: Vec::new(),
                    tool_results: Vec::new(),
                    duration_ms: 120_000,
                    usage: None,
                },
            ],
            plan: None,
            total_tool_calls: 0,
            total_steps: 1,
            total_usage: None,
            total_duration_ms: 120_000,
            completed: false,
            error: Some("Execution timed out".to_string()),
            model: "default".to_string(),
        };

        assert!(!result.completed);
        assert!(result.error.is_some());
        assert_eq!(result.steps[0].step_type, AgentStepType::Error);
    }

    #[test]
    fn test_data_manager_template_write_mode() {
        let template = get_builtin_template("data_manager").unwrap();
        assert!(!template.config.readonly_mode.unwrap());
        assert!(template.tools.contains(&"db_doc_crud".to_string()));

        let mut config = AgentConfig::default();
        apply_template(&mut config, &template);
        assert!(!config.readonly_mode);
    }
}
