use crate::cron;
use crate::security::SecurityPolicy;
use anyhow::Result;
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use serde_json::json;
use std::sync::Arc;
use zeroclaw_api::tool::{Tool, ToolOutput, ToolResult};
use zeroclaw_config::schema::Config;

/// Tool that lets the agent manage recurring and one-shot scheduled tasks.
pub struct ScheduleTool {
    security: Arc<SecurityPolicy>,
    config: Config,
    /// Owning agent — risk profile gate for shell command validation.
    agent_alias: String,
}

impl ScheduleTool {
    pub fn new(
        security: Arc<SecurityPolicy>,
        config: Config,
        agent_alias: impl Into<String>,
    ) -> Self {
        Self {
            security,
            config,
            agent_alias: agent_alias.into(),
        }
    }
}

#[async_trait]
impl Tool for ScheduleTool {
    fn name(&self) -> &str {
        "schedule"
    }

    fn description(&self) -> &str {
        "Manage scheduled shell-only tasks. Actions: create/add/once/list/get/cancel/remove/pause/resume. \
         WARNING: This tool creates shell jobs whose output is only logged, NOT delivered to any channel. \
         To send a scheduled message to Discord/Telegram/Slack/Matrix, use the cron_add tool with job_type='agent' \
         and a delivery config like {\"mode\":\"announce\",\"channel\":\"discord\",\"to\":\"<channel_id>\"}."
    }

    fn parameters_schema(&self) -> serde_json::Value {
        json!({
            "type": "object",
            "properties": {
                "action": {
                    "type": "string",
                    "enum": ["create", "add", "once", "list", "get", "cancel", "remove", "pause", "resume"],
                    "description": "Action to perform"
                },
                "expression": {
                    "type": "string",
                    "description": "Cron expression for recurring tasks (e.g. '*/5 * * * *')."
                },
                "delay": {
                    "type": "string",
                    "description": "Delay for one-shot tasks (e.g. '30m', '2h', '1d')."
                },
                "run_at": {
                    "type": "string",
                    "description": "Absolute RFC3339 time for one-shot tasks (e.g. '2030-01-01T00:00:00Z')."
                },
                "command": {
                    "type": "string",
                    "description": "Shell command to execute. Required for create/add/once."
                },
                "approved": {
                    "type": "boolean",
                    "description": "Set true to explicitly approve medium/high-risk shell commands in supervised mode",
                    "default": false
                },
                "id": {
                    "type": "string",
                    "description": "Task ID. Required for get/cancel/remove/pause/resume."
                }
            },
            "required": ["action"]
        })
    }

    async fn execute(&self, args: serde_json::Value) -> Result<ToolResult> {
        let action = args
            .get("action")
            .and_then(|value| value.as_str())
            .ok_or_else(|| {
                ::zeroclaw_log::record!(
                    WARN,
                    ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
                        .with_outcome(::zeroclaw_log::EventOutcome::Failure)
                        .with_attrs(::serde_json::json!({"param": "action"})),
                    "tool argument validation failed"
                );

                anyhow::Error::msg("Missing 'action' parameter")
            })?;

        match action {
            "list" => self.handle_list(),
            "get" => {
                let id = args
                    .get("id")
                    .and_then(|value| value.as_str())
                    .ok_or_else(|| {
                        ::zeroclaw_log::record!(
                            WARN,
                            ::zeroclaw_log::Event::new(
                                module_path!(),
                                ::zeroclaw_log::Action::Reject
                            )
                            .with_outcome(::zeroclaw_log::EventOutcome::Failure)
                            .with_attrs(::serde_json::json!({"param": "id"})),
                            "tool argument validation failed"
                        );

                        anyhow::Error::msg("Missing 'id' parameter for get action")
                    })?;
                self.handle_get(id)
            }
            "create" | "add" | "once" => {
                let approved = args
                    .get("approved")
                    .and_then(serde_json::Value::as_bool)
                    .unwrap_or(false);
                self.handle_create_like(action, &args, approved)
            }
            "cancel" | "remove" => {
                if let Some(blocked) = self.enforce_mutation_allowed(action) {
                    return Ok(blocked);
                }
                let id = args
                    .get("id")
                    .and_then(|value| value.as_str())
                    .ok_or_else(|| {
                        ::zeroclaw_log::record!(
                            WARN,
                            ::zeroclaw_log::Event::new(
                                module_path!(),
                                ::zeroclaw_log::Action::Reject
                            )
                            .with_outcome(::zeroclaw_log::EventOutcome::Failure)
                            .with_attrs(::serde_json::json!({"param": "id"})),
                            "tool argument validation failed"
                        );

                        anyhow::Error::msg("Missing 'id' parameter for cancel action")
                    })?;
                Ok(self.handle_cancel(id))
            }
            "pause" => {
                if let Some(blocked) = self.enforce_mutation_allowed(action) {
                    return Ok(blocked);
                }
                let id = args
                    .get("id")
                    .and_then(|value| value.as_str())
                    .ok_or_else(|| {
                        ::zeroclaw_log::record!(
                            WARN,
                            ::zeroclaw_log::Event::new(
                                module_path!(),
                                ::zeroclaw_log::Action::Reject
                            )
                            .with_outcome(::zeroclaw_log::EventOutcome::Failure)
                            .with_attrs(::serde_json::json!({"param": "id"})),
                            "tool argument validation failed"
                        );

                        anyhow::Error::msg("Missing 'id' parameter for pause action")
                    })?;
                Ok(self.handle_pause_resume(id, true))
            }
            "resume" => {
                if let Some(blocked) = self.enforce_mutation_allowed(action) {
                    return Ok(blocked);
                }
                let id = args
                    .get("id")
                    .and_then(|value| value.as_str())
                    .ok_or_else(|| {
                        ::zeroclaw_log::record!(
                            WARN,
                            ::zeroclaw_log::Event::new(
                                module_path!(),
                                ::zeroclaw_log::Action::Reject
                            )
                            .with_outcome(::zeroclaw_log::EventOutcome::Failure)
                            .with_attrs(::serde_json::json!({"param": "id"})),
                            "tool argument validation failed"
                        );

                        anyhow::Error::msg("Missing 'id' parameter for resume action")
                    })?;
                Ok(self.handle_pause_resume(id, false))
            }
            other => Ok(ToolResult {
                success: false,
                output: ToolOutput::default(),
                error: Some(format!(
                    "Unknown action '{other}'. Use create/add/once/list/get/cancel/remove/pause/resume."
                )),
            }),
        }
    }
}

impl ScheduleTool {
    fn enforce_mutation_allowed(&self, action: &str) -> Option<ToolResult> {
        if !self.config.scheduler.enabled {
            return Some(ToolResult {
                success: false,
                output: ToolOutput::default(),
                error: Some(format!(
                    "cron is disabled by config (scheduler.enabled=false); cannot perform '{action}'"
                )),
            });
        }

        if !self.security.can_act() {
            return Some(ToolResult {
                success: false,
                output: ToolOutput::default(),
                error: Some(format!(
                    "Security policy: read-only mode, cannot perform '{action}'"
                )),
            });
        }

        if !self.security.record_action() {
            return Some(ToolResult {
                success: false,
                output: ToolOutput::default(),
                error: Some("Rate limit exceeded: action budget exhausted".to_string()),
            });
        }

        None
    }

    fn handle_list(&self) -> Result<ToolResult> {
        let jobs = cron::list_jobs(&self.config)?;
        if jobs.is_empty() {
            return Ok(ToolResult {
                success: true,
                output: "No scheduled jobs.".to_string().into(),
                error: None,
            });
        }

        let mut lines = Vec::with_capacity(jobs.len());
        for job in jobs {
            let paused = !job.enabled;
            let one_shot = matches!(job.schedule, cron::Schedule::At { .. });
            let flags = match (paused, one_shot) {
                (true, true) => " [disabled, one-shot]",
                (true, false) => " [disabled]",
                (false, true) => " [one-shot]",
                (false, false) => "",
            };
            let last_run = job
                .last_run
                .map_or_else(|| "never".to_string(), |value| value.to_rfc3339());
            let last_status = job.last_status.unwrap_or_else(|| "n/a".to_string());
            lines.push(format!(
                "- {} | {} | next={} | last={} ({}){} | cmd: {}",
                job.id,
                job.expression,
                job.next_run.to_rfc3339(),
                last_run,
                last_status,
                flags,
                job.command
            ));
        }

        Ok(ToolResult {
            success: true,
            output: format!("Scheduled jobs ({}):\n{}", lines.len(), lines.join("\n")).into(),
            error: None,
        })
    }

    fn handle_get(&self, id: &str) -> Result<ToolResult> {
        match cron::get_job(&self.config, id) {
            Ok(job) => {
                let detail = json!({
                    "id": job.id,
                    "expression": job.expression,
                    "command": job.command,
                    "next_run": job.next_run.to_rfc3339(),
                    "last_run": job.last_run.map(|value| value.to_rfc3339()),
                    "last_status": job.last_status,
                    "enabled": job.enabled,
                    "one_shot": matches!(job.schedule, cron::Schedule::At { .. }),
                });
                Ok(ToolResult {
                    success: true,
                    output: serde_json::to_string_pretty(&detail)?.into(),
                    error: None,
                })
            }
            Err(_) => Ok(ToolResult {
                success: false,
                output: ToolOutput::default(),
                error: Some(format!("Job '{id}' not found")),
            }),
        }
    }

    fn handle_create_like(
        &self,
        action: &str,
        args: &serde_json::Value,
        approved: bool,
    ) -> Result<ToolResult> {
        let command = args
            .get("command")
            .and_then(|value| value.as_str())
            .filter(|value| !value.trim().is_empty())
            .ok_or_else(|| {
                ::zeroclaw_log::record!(
                    WARN,
                    ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
                        .with_outcome(::zeroclaw_log::EventOutcome::Failure)
                        .with_attrs(::serde_json::json!({"param": "command"})),
                    "tool argument validation failed"
                );

                anyhow::Error::msg("Missing or empty 'command' parameter")
            })?;

        let expression = args.get("expression").and_then(|value| value.as_str());
        let delay = args.get("delay").and_then(|value| value.as_str());
        let run_at = args.get("run_at").and_then(|value| value.as_str());

        match action {
            "add" => {
                if expression.is_none() || delay.is_some() || run_at.is_some() {
                    return Ok(ToolResult {
                        success: false,
                        output: ToolOutput::default(),
                        error: Some("'add' requires 'expression' and forbids delay/run_at".into()),
                    });
                }
            }
            "once" => {
                if expression.is_some() || (delay.is_none() && run_at.is_none()) {
                    return Ok(ToolResult {
                        success: false,
                        output: ToolOutput::default(),
                        error: Some("'once' requires exactly one of 'delay' or 'run_at'".into()),
                    });
                }
                if delay.is_some() && run_at.is_some() {
                    return Ok(ToolResult {
                        success: false,
                        output: ToolOutput::default(),
                        error: Some("'once' supports either delay or run_at, not both".into()),
                    });
                }
            }
            _ => {
                let count = [expression.is_some(), delay.is_some(), run_at.is_some()]
                    .into_iter()
                    .filter(|value| *value)
                    .count();
                if count != 1 {
                    return Ok(ToolResult {
                        success: false,
                        output: ToolOutput::default(),
                        error: Some(
                            "Exactly one of 'expression', 'delay', or 'run_at' must be provided"
                                .into(),
                        ),
                    });
                }
            }
        }

        // Enforce rate-limiting AFTER command/args validation so that invalid
        // requests do not consume the action budget.
        if let Some(blocked) = self.enforce_mutation_allowed(action) {
            return Ok(blocked);
        }

        // All job creation routes through validated cron helpers, which enforce
        // the full security policy (allowlist + risk gate) before persistence.
        if let Some(value) = expression {
            let job = match cron::add_shell_job_with_approval(
                &self.config,
                &self.agent_alias,
                None,
                cron::Schedule::Cron {
                    expr: value.to_string(),
                    tz: None,
                },
                command,
                None,
                approved,
            ) {
                Ok(job) => job,
                Err(error) => {
                    return Ok(ToolResult {
                        success: false,
                        output: ToolOutput::default(),
                        error: Some(error.to_string()),
                    });
                }
            };
            return Ok(ToolResult {
                success: true,
                output: format!(
                    "Created recurring job {} (expr: {}, next: {}, cmd: {})",
                    job.id,
                    job.expression,
                    job.next_run.to_rfc3339(),
                    job.command
                )
                .into(),
                error: None,
            });
        }

        if let Some(value) = delay {
            let job = match cron::add_once_validated(
                &self.config,
                &self.agent_alias,
                value,
                command,
                approved,
            ) {
                Ok(job) => job,
                Err(error) => {
                    return Ok(ToolResult {
                        success: false,
                        output: ToolOutput::default(),
                        error: Some(error.to_string()),
                    });
                }
            };
            return Ok(ToolResult {
                success: true,
                output: format!(
                    "Created one-shot job {} (runs at: {}, cmd: {})",
                    job.id,
                    job.next_run.to_rfc3339(),
                    job.command
                )
                .into(),
                error: None,
            });
        }

        let run_at_raw = run_at.ok_or_else(|| {
            ::zeroclaw_log::record!(
                WARN,
                ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
                    .with_outcome(::zeroclaw_log::EventOutcome::Failure),
                "schedule tool: missing scheduling parameters (run_at / delay_seconds)"
            );
            anyhow::Error::msg("Missing scheduling parameters")
        })?;
        let run_at_parsed: DateTime<Utc> = DateTime::parse_from_rfc3339(run_at_raw)
            .map_err(|error| {
                ::zeroclaw_log::record!(
                    WARN,
                    ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
                        .with_outcome(::zeroclaw_log::EventOutcome::Failure)
                        .with_attrs(::serde_json::json!({
                            "run_at": run_at_raw,
                            "error": format!("{}", error),
                        })),
                    "schedule tool: invalid run_at timestamp"
                );
                anyhow::Error::msg(format!("Invalid run_at timestamp: {error}"))
            })?
            .with_timezone(&Utc);

        let job = match cron::add_once_at_validated(
            &self.config,
            &self.agent_alias,
            run_at_parsed,
            command,
            approved,
        ) {
            Ok(job) => job,
            Err(error) => {
                return Ok(ToolResult {
                    success: false,
                    output: ToolOutput::default(),
                    error: Some(error.to_string()),
                });
            }
        };
        Ok(ToolResult {
            success: true,
            output: format!(
                "Created one-shot job {} (runs at: {}, cmd: {})",
                job.id,
                job.next_run.to_rfc3339(),
                job.command
            )
            .into(),
            error: None,
        })
    }

    fn handle_cancel(&self, id: &str) -> ToolResult {
        match cron::remove_job(&self.config, id) {
            Ok(()) => ToolResult {
                success: true,
                output: format!("Cancelled job {id}").into(),
                error: None,
            },
            Err(error) => ToolResult {
                success: false,
                output: ToolOutput::default(),
                error: Some(error.to_string()),
            },
        }
    }

    fn handle_pause_resume(&self, id: &str, pause: bool) -> ToolResult {
        let operation = if pause {
            cron::pause_job(&self.config, id)
        } else {
            cron::resume_job(&self.config, id)
        };

        match operation {
            Ok(_) => ToolResult {
                success: true,
                output: if pause {
                    format!("Paused job {id}").into()
                } else {
                    format!("Resumed job {id}").into()
                },
                error: None,
            },
            Err(error) => ToolResult {
                success: false,
                output: ToolOutput::default(),
                error: Some(error.to_string()),
            },
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::security::AutonomyLevel;
    use tempfile::TempDir;

    async fn test_setup() -> (TempDir, Config, Arc<SecurityPolicy>) {
        let tmp = TempDir::new().unwrap();
        // Seed test-agent so ScheduleTool's add_shell_job_with_approval
        // (which validates against the agent's risk profile) succeeds.
        let config = config_with_test_agent_profiles(
            tmp.path().join("workspace"),
            tmp.path().join("config.toml"),
            zeroclaw_config::schema::RiskProfileConfig::default(),
            zeroclaw_config::schema::RuntimeProfileConfig::default(),
        );
        tokio::fs::create_dir_all(&config.data_dir).await.unwrap();
        let security = Arc::new(SecurityPolicy::for_agent(&config, TEST_AGENT).unwrap());
        (tmp, config, security)
    }

    #[tokio::test]
    async fn tool_name_and_schema() {
        let (_tmp, config, security) = test_setup().await;
        let tool = ScheduleTool::new(security, config, TEST_AGENT);
        assert_eq!(tool.name(), "schedule");
        let schema = tool.parameters_schema();
        assert!(schema["properties"]["action"].is_object());
    }

    #[tokio::test]
    async fn list_empty() {
        let (_tmp, config, security) = test_setup().await;
        let tool = ScheduleTool::new(security, config, TEST_AGENT);

        let result = tool.execute(json!({"action": "list"})).await.unwrap();
        assert!(result.success);
        assert!(result.output.contains("No scheduled jobs"));
    }

    #[tokio::test]
    async fn create_get_and_cancel_roundtrip() {
        let (_tmp, config, security) = test_setup().await;
        let tool = ScheduleTool::new(security, config, TEST_AGENT);

        let create = tool
            .execute(json!({
                "action": "create",
                "expression": "*/5 * * * *",
                "command": "echo hello"
            }))
            .await
            .unwrap();
        assert!(create.success);
        assert!(create.output.contains("Created recurring job"));

        let list = tool.execute(json!({"action": "list"})).await.unwrap();
        assert!(list.success);
        assert!(list.output.contains("echo hello"));

        let id = create.output.split_whitespace().nth(3).unwrap();

        let get = tool
            .execute(json!({"action": "get", "id": id}))
            .await
            .unwrap();
        assert!(get.success);
        assert!(get.output.contains("echo hello"));

        let cancel = tool
            .execute(json!({"action": "cancel", "id": id}))
            .await
            .unwrap();
        assert!(cancel.success);
    }

    #[tokio::test]
    async fn once_and_pause_resume_aliases_work() {
        let (_tmp, config, security) = test_setup().await;
        let tool = ScheduleTool::new(security, config, TEST_AGENT);

        let once = tool
            .execute(json!({
                "action": "once",
                "delay": "30m",
                "command": "echo delayed"
            }))
            .await
            .unwrap();
        assert!(once.success);

        let add = tool
            .execute(json!({
                "action": "add",
                "expression": "*/10 * * * *",
                "command": "echo recurring"
            }))
            .await
            .unwrap();
        assert!(add.success);

        let id = add.output.split_whitespace().nth(3).unwrap();
        let pause = tool
            .execute(json!({"action": "pause", "id": id}))
            .await
            .unwrap();
        assert!(pause.success);

        let resume = tool
            .execute(json!({"action": "resume", "id": id}))
            .await
            .unwrap();
        assert!(resume.success);
    }

    const TEST_AGENT: &str = "test-agent";

    fn config_with_test_agent_profiles(
        workspace: std::path::PathBuf,
        config_path: std::path::PathBuf,
        risk: zeroclaw_config::schema::RiskProfileConfig,
        runtime: zeroclaw_config::schema::RuntimeProfileConfig,
    ) -> Config {
        let mut config = Config {
            data_dir: workspace,
            config_path,
            ..Config::default()
        };
        config.risk_profiles.insert(TEST_AGENT.into(), risk);
        config.runtime_profiles.insert(TEST_AGENT.into(), runtime);
        seed_test_agent_provider_and_agent(&mut config);
        config
    }

    fn seed_test_agent_provider_and_agent(config: &mut Config) {
        config
            .risk_profiles
            .entry(TEST_AGENT.to_string())
            .or_default();
        config
            .runtime_profiles
            .entry(TEST_AGENT.to_string())
            .or_default();
        config
            .providers
            .models
            .ensure("openrouter", TEST_AGENT)
            .expect("known family");
        config.agents.entry(TEST_AGENT.to_string()).or_insert(
            zeroclaw_config::schema::AliasedAgentConfig {
                model_provider: format!("openrouter.{TEST_AGENT}").into(),
                risk_profile: TEST_AGENT.into(),
                runtime_profile: TEST_AGENT.into(),
                ..Default::default()
            },
        );
    }

    #[tokio::test]
    async fn readonly_blocks_mutating_actions() {
        let tmp = TempDir::new().unwrap();
        let config = config_with_test_agent_profiles(
            tmp.path().join("workspace"),
            tmp.path().join("config.toml"),
            zeroclaw_config::schema::RiskProfileConfig {
                level: AutonomyLevel::ReadOnly,
                ..Default::default()
            },
            zeroclaw_config::schema::RuntimeProfileConfig::default(),
        );
        tokio::fs::create_dir_all(&config.data_dir).await.unwrap();
        let security = Arc::new(SecurityPolicy::for_agent(&config, TEST_AGENT).unwrap());

        let tool = ScheduleTool::new(security, config, TEST_AGENT);

        let blocked = tool
            .execute(json!({
                "action": "create",
                "expression": "* * * * *",
                "command": "echo blocked"
            }))
            .await
            .unwrap();
        assert!(!blocked.success);
        assert!(blocked.error.as_deref().unwrap().contains("read-only"));

        let list = tool.execute(json!({"action": "list"})).await.unwrap();
        assert!(list.success);
    }

    #[tokio::test]
    async fn rate_limit_blocks_create_action() {
        let tmp = TempDir::new().unwrap();
        let config = config_with_test_agent_profiles(
            tmp.path().join("workspace"),
            tmp.path().join("config.toml"),
            zeroclaw_config::schema::RiskProfileConfig {
                level: AutonomyLevel::Full,
                ..Default::default()
            },
            zeroclaw_config::schema::RuntimeProfileConfig {
                max_actions_per_hour: 0,
                ..Default::default()
            },
        );
        tokio::fs::create_dir_all(&config.data_dir).await.unwrap();
        let security = Arc::new(SecurityPolicy::for_agent(&config, TEST_AGENT).unwrap());
        let tool = ScheduleTool::new(security, config, TEST_AGENT);

        let blocked = tool
            .execute(json!({
                "action": "create",
                "expression": "*/5 * * * *",
                "command": "echo blocked-by-rate-limit"
            }))
            .await
            .unwrap();
        assert!(!blocked.success);
        assert!(
            blocked
                .error
                .as_deref()
                .unwrap_or_default()
                .contains("Rate limit exceeded")
        );

        let list = tool.execute(json!({"action": "list"})).await.unwrap();
        assert!(list.success);
        assert!(list.output.contains("No scheduled jobs"));
    }

    #[tokio::test]
    async fn rate_limit_blocks_cancel_and_keeps_job() {
        let tmp = TempDir::new().unwrap();
        let config = config_with_test_agent_profiles(
            tmp.path().join("workspace"),
            tmp.path().join("config.toml"),
            zeroclaw_config::schema::RiskProfileConfig {
                level: AutonomyLevel::Full,
                ..Default::default()
            },
            zeroclaw_config::schema::RuntimeProfileConfig {
                max_actions_per_hour: 1,
                ..Default::default()
            },
        );
        tokio::fs::create_dir_all(&config.data_dir).await.unwrap();
        let security = Arc::new(SecurityPolicy::for_agent(&config, TEST_AGENT).unwrap());
        let tool = ScheduleTool::new(security, config, TEST_AGENT);

        let create = tool
            .execute(json!({
                "action": "create",
                "expression": "*/5 * * * *",
                "command": "echo keep-me"
            }))
            .await
            .unwrap();
        assert!(create.success);
        let id = create.output.split_whitespace().nth(3).unwrap();

        let cancel = tool
            .execute(json!({"action": "cancel", "id": id}))
            .await
            .unwrap();
        assert!(!cancel.success);
        assert!(
            cancel
                .error
                .as_deref()
                .unwrap_or_default()
                .contains("Rate limit exceeded")
        );

        let get = tool
            .execute(json!({"action": "get", "id": id}))
            .await
            .unwrap();
        assert!(get.success);
        assert!(get.output.contains("echo keep-me"));
    }

    #[tokio::test]
    async fn unknown_action_returns_failure() {
        let (_tmp, config, security) = test_setup().await;
        let tool = ScheduleTool::new(security, config, TEST_AGENT);

        let result = tool.execute(json!({"action": "explode"})).await.unwrap();
        assert!(!result.success);
        assert!(result.error.as_deref().unwrap().contains("Unknown action"));
    }

    #[tokio::test]
    async fn mutating_actions_fail_when_cron_disabled() {
        let tmp = TempDir::new().unwrap();
        let mut config = Config {
            data_dir: tmp.path().join("data"),
            config_path: tmp.path().join("config.toml"),
            ..Config::default()
        };
        config.scheduler.enabled = false;
        seed_test_agent_provider_and_agent(&mut config);
        std::fs::create_dir_all(&config.data_dir).unwrap();
        let security = Arc::new(SecurityPolicy::for_agent(&config, TEST_AGENT).unwrap());
        let tool = ScheduleTool::new(security, config, TEST_AGENT);

        let create = tool
            .execute(json!({
                "action": "create",
                "expression": "*/5 * * * *",
                "command": "echo hello"
            }))
            .await
            .unwrap();

        assert!(!create.success);
        assert!(
            create
                .error
                .as_deref()
                .unwrap_or_default()
                .contains("cron is disabled")
        );
    }

    #[tokio::test]
    async fn create_blocks_disallowed_command() {
        let tmp = TempDir::new().unwrap();
        let mut config = Config {
            data_dir: tmp.path().join("data"),
            config_path: tmp.path().join("config.toml"),
            ..Config::default()
        };
        config
            .risk_profiles
            .entry(TEST_AGENT.into())
            .or_default()
            .level = AutonomyLevel::Supervised;
        config
            .risk_profiles
            .entry(TEST_AGENT.into())
            .or_default()
            .allowed_commands = vec!["echo".into()];
        seed_test_agent_provider_and_agent(&mut config);
        std::fs::create_dir_all(&config.data_dir).unwrap();
        let security = Arc::new(SecurityPolicy::for_agent(&config, TEST_AGENT).unwrap());
        let tool = ScheduleTool::new(security, config, TEST_AGENT);

        let result = tool
            .execute(json!({
                "action": "create",
                "expression": "*/5 * * * *",
                "command": "curl https://example.com"
            }))
            .await
            .unwrap();

        assert!(!result.success);
        assert!(
            result
                .error
                .as_deref()
                .unwrap_or_default()
                .contains("not allowed")
        );
    }

    #[tokio::test]
    async fn medium_risk_create_requires_approval() {
        let tmp = TempDir::new().unwrap();
        let mut config = Config {
            data_dir: tmp.path().join("data"),
            config_path: tmp.path().join("config.toml"),
            ..Config::default()
        };
        config
            .risk_profiles
            .entry(TEST_AGENT.into())
            .or_default()
            .level = AutonomyLevel::Supervised;
        config
            .risk_profiles
            .entry(TEST_AGENT.into())
            .or_default()
            .allowed_commands = vec!["touch".into()];
        seed_test_agent_provider_and_agent(&mut config);
        std::fs::create_dir_all(&config.data_dir).unwrap();
        let security = Arc::new(SecurityPolicy::for_agent(&config, TEST_AGENT).unwrap());
        let tool = ScheduleTool::new(security, config, TEST_AGENT);

        let denied = tool
            .execute(json!({
                "action": "create",
                "expression": "*/5 * * * *",
                "command": "touch schedule-policy-test"
            }))
            .await
            .unwrap();
        assert!(!denied.success);
        assert!(
            denied
                .error
                .as_deref()
                .unwrap_or_default()
                .contains("explicit approval")
        );

        let approved = tool
            .execute(json!({
                "action": "create",
                "expression": "*/5 * * * *",
                "command": "touch schedule-policy-test",
                "approved": true
            }))
            .await
            .unwrap();
        assert!(approved.success, "{:?}", approved.error);
    }
}
