use std::collections::HashSet;
use std::sync::Arc;

use async_trait::async_trait;
use serde_json::json;

use crate::config::SchedulerSchedule;
use crate::storage::{
    SchedulerJobRecord, SchedulerJobRepository, SchedulerJobState, SchedulerJobUpsert,
};
use crate::tools::traits::{Tool, ToolResult};
use crate::tools::{extract_bool, extract_i64};

pub struct SchedulerManageTool {
    jobs: Arc<dyn SchedulerJobRepository>,
    known_agents: Arc<HashSet<String>>,
}

impl SchedulerManageTool {
    pub fn new(jobs: Arc<dyn SchedulerJobRepository>, known_agents: HashSet<String>) -> Self {
        Self {
            jobs,
            known_agents: Arc::new(known_agents),
        }
    }
}

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

    fn description(&self) -> &str {
        "Manage repository-backed scheduled jobs. Supports actions: list, get, put, update, delete, pause, resume. Jobs are persisted by the configured scheduler job repository and executed by the scheduler runtime. \
        \
        When creating agent_task or silent_agent_task jobs, keep prompt/system_prompt focused on the work to perform; do not restate execution times unless the task logic truly depends on them, because the trigger already controls timing. For cron schedules, standard cron syntax is supported: use 1-5 for Monday-Friday, 0 or 7 for Sunday. \
        \
        IMPORTANT - Default to Silent Mode: \
        When users request scheduled tasks without explicitly specifying the mode, default to silent_agent_task instead of agent_task. Silent mode is preferred for most automated tasks because it runs in a dedicated background session without cluttering the main conversation. Only use agent_task when the user explicitly wants interactive execution in the main chat. \
        \
        IMPORTANT - Target Configuration: \
        For agent_task and silent_agent_task, the target.channel and target.chat_id determine where notifications are sent. \
        - If target is omitted or fields are empty, they are automatically filled from the current conversation context. \
        - To send to the current conversation: omit target or set target fields to null/empty. \
        - To send to a specific target: explicitly provide channel (e.g., 'feishu', 'wechat', 'cli') and chat_id (e.g., 'oc_xxx'). \
        - Do NOT use placeholder values like 'current' or 'default' - these will be treated as literal values and cause message delivery failures."
    }

    fn parameters_schema(&self) -> serde_json::Value {
        let mut allowed_agents = self.known_agents.iter().cloned().collect::<Vec<_>>();
        allowed_agents.sort();
        let agent_hint = if allowed_agents.is_empty() {
            "agent_task payload.agent may be omitted or set to 'default'.".to_string()
        } else {
            format!(
                "agent_task payload.agent may be omitted, set to 'default', or use one of configured agents: {}.",
                allowed_agents.join(", ")
            )
        };

        json!({
            "type": "object",
            "properties": {
                "action": {
                    "type": "string",
                    "enum": ["list", "get", "put", "update", "delete", "pause", "resume"]
                },
                "id": {
                    "type": "string",
                    "description": "Job id"
                },
                "enabled_only": {
                    "type": "boolean",
                    "description": "Only for list action"
                },
                "kind": {
                    "type": "string",
                    "enum": ["internal_event", "outbound_message", "agent_task", "silent_agent_task"]
                },
                "schedule": {
                    "type": "object",
                    "description": "Schedule object, for example {type: 'interval', seconds: 300} or {type: 'cron', expression: '0 9 * * *'}"
                },
                "target": {
                    "type": "object",
                    "description": "Target routing. agent_task and silent_agent_task require channel and chat_id. silent_agent_task may optionally set session_chat_id to choose a dedicated background session; otherwise one is derived from the job id."
                },
                "payload": {
                    "type": "object",
                    "description": format!("Job payload. agent_task and silent_agent_task support prompt, agent, fresh_session, system_prompt, sender_id, metadata. For these kinds, write prompt/system_prompt as execution instructions and avoid repeating schedule phrases or execution times such as 每天9点 or 每小时 unless the task itself must reason about time. {} outbound_message expects content. internal_event expects event.", agent_hint)
                },
                "max_runs": {
                    "type": ["integer", "null"]
                }
            },
            "required": ["action"]
        })
    }

    async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {
        self.execute_with_context(&crate::tools::ToolContext::default(), args)
            .await
    }

    async fn execute_with_context(
        &self,
        context: &crate::tools::ToolContext,
        args: serde_json::Value,
    ) -> anyhow::Result<ToolResult> {
        if args.is_null() {
            return Ok(error_result(
                "Missing required parameters: scheduler_manage expects a JSON object like {\"action\":\"list\"}",
            ));
        }

        if !args.is_object() {
            return Ok(error_result(
                "Invalid parameters: scheduler_manage expects a JSON object",
            ));
        }

        let action = match args.get("action").and_then(|value| value.as_str()) {
            Some(action) => action,
            None => return Ok(error_result("Missing required parameter: action")),
        };

        let output = match action {
            "list" => {
                let enabled_only = extract_bool(&args, "enabled_only").unwrap_or(false);
                let jobs = self.jobs.list_scheduler_jobs(enabled_only)?;
                json!(jobs.iter().map(record_to_json).collect::<Vec<_>>())
            }
            "get" => {
                let id = require_str(&args, "id")?;
                match self.jobs.get_scheduler_job(id)? {
                    Some(record) => record_to_json(&record),
                    None => return Ok(error_result(&format!("scheduler job '{}' not found", id))),
                }
            }
            "put" => {
                let input = build_upsert(context, &args, &self.known_agents)?;
                let record = self.jobs.upsert_scheduler_job(&input)?;
                record_to_json(&record)
            }
            "delete" => {
                let id = require_str(&args, "id")?;
                self.jobs.delete_scheduler_job(id)?;
                json!({"status": "deleted", "id": id})
            }
            "pause" => {
                let id = require_str(&args, "id")?;
                let record = self
                    .jobs
                    .get_scheduler_job(id)?
                    .ok_or_else(|| anyhow::anyhow!("scheduler job '{}' not found", id))?;
                let mut input = record_to_upsert(&record);
                input.enabled = false;
                input.state = SchedulerJobState::Paused;
                input.paused_at = Some(current_timestamp());
                input.next_fire_at = None;
                let saved = self.jobs.upsert_scheduler_job(&input)?;
                record_to_json(&saved)
            }
            "resume" => {
                let id = require_str(&args, "id")?;
                let record = self
                    .jobs
                    .get_scheduler_job(id)?
                    .ok_or_else(|| anyhow::anyhow!("scheduler job '{}' not found", id))?;
                let mut input = record_to_upsert(&record);
                input.enabled = true;
                input.state = SchedulerJobState::Scheduled;
                input.paused_at = None;
                input.completed_at = None;
                input.next_fire_at = None;
                let saved = self.jobs.upsert_scheduler_job(&input)?;
                record_to_json(&saved)
            }
            "update" => {
                let id = require_str(&args, "id")?;
                let record = self
                    .jobs
                    .get_scheduler_job(id)?
                    .ok_or_else(|| anyhow::anyhow!("scheduler job '{}' not found", id))?;
                let input = build_update_upsert(context, &args, &self.known_agents, &record)?;
                let saved = self.jobs.upsert_scheduler_job(&input)?;
                record_to_json(&saved)
            }
            _ => return Ok(error_result("Unsupported action")),
        };

        Ok(ToolResult {
            success: true,
            output: serde_json::to_string_pretty(&output)?,
            error: None,
        })
    }
}

fn build_upsert(
    context: &crate::tools::ToolContext,
    args: &serde_json::Value,
    known_agents: &HashSet<String>,
) -> anyhow::Result<SchedulerJobUpsert> {
    let id = require_str(args, "id")?.to_string();
    let kind = require_str(args, "kind")?.to_string();
    let schedule_value = args
        .get("schedule")
        .cloned()
        .ok_or_else(|| anyhow::anyhow!("Missing required parameter: schedule"))?;
    let schedule: SchedulerSchedule = serde_json::from_value(schedule_value.clone())?;
    schedule.validate(&id)?;

    let (interval_secs, startup_delay_secs) = match &schedule {
        SchedulerSchedule::Interval {
            seconds,
            startup_delay_secs,
        } => (*seconds as i64, *startup_delay_secs as i64),
        _ => (0, 0),
    };

    let payload = args.get("payload").cloned().unwrap_or_else(|| json!({}));
    let target = enrich_target_from_context(
        args.get("target").cloned().unwrap_or_else(|| json!({})),
        context,
    );
    if kind == "agent_task" || kind == "silent_agent_task" {
        validate_agent_task_payload(&payload, known_agents)?;
        validate_target_fields(&target, &["channel", "chat_id"], &kind)?;
    } else if kind == "outbound_message" {
        validate_outbound_message_payload(&payload)?;
        validate_target_fields(&target, &["channel", "chat_id"], "outbound_message")?;
    }

    Ok(SchedulerJobUpsert {
        id,
        kind,
        schedule: serde_json::to_value(schedule)?,
        interval_secs,
        startup_delay_secs,
        target,
        payload,
        enabled: extract_bool(args, "enabled").unwrap_or(true),
        state: if extract_bool(args, "enabled").unwrap_or(true) {
            SchedulerJobState::Scheduled
        } else {
            SchedulerJobState::Paused
        },
        last_status: None,
        last_error: None,
        run_count: 0,
        max_runs: extract_i64(args, "max_runs"),
        last_fired_at: None,
        next_fire_at: None,
        paused_at: None,
        completed_at: None,
    })
}

fn build_update_upsert(
    context: &crate::tools::ToolContext,
    args: &serde_json::Value,
    known_agents: &HashSet<String>,
    existing: &crate::storage::SchedulerJobRecord,
) -> anyhow::Result<SchedulerJobUpsert> {
    let mut upsert = record_to_upsert(existing);

    if let Some(schedule_value) = args.get("schedule") {
        let schedule: SchedulerSchedule = serde_json::from_value(schedule_value.clone())?;
        schedule.validate(&upsert.id)?;
        upsert.schedule = serde_json::to_value(&schedule)?;
        let (interval_secs, startup_delay_secs) = match &schedule {
            SchedulerSchedule::Interval {
                seconds,
                startup_delay_secs,
            } => (*seconds as i64, *startup_delay_secs as i64),
            _ => (0, 0),
        };
        upsert.interval_secs = interval_secs;
        upsert.startup_delay_secs = startup_delay_secs;
        upsert.next_fire_at = None;
    }

    if args.get("target").is_some() {
        upsert.target = enrich_target_from_context(
            args.get("target").cloned().unwrap_or_else(|| json!({})),
            context,
        );
    }

    if let Some(payload) = args.get("payload") {
        upsert.payload = payload.clone();
    }

    if let Some(enabled) = extract_bool(args, "enabled") {
        upsert.enabled = enabled;
        upsert.state = if enabled {
            SchedulerJobState::Scheduled
        } else {
            SchedulerJobState::Paused
        };
        if !enabled {
            upsert.paused_at = Some(current_timestamp());
        } else {
            upsert.paused_at = None;
            upsert.completed_at = None;
        }
        upsert.next_fire_at = None;
    }

    if args.get("max_runs").is_some() {
        upsert.max_runs = extract_i64(args, "max_runs");
    }

    if upsert.kind == "agent_task" || upsert.kind == "silent_agent_task" {
        validate_agent_task_payload(&upsert.payload, known_agents)?;
        validate_target_fields(&upsert.target, &["channel", "chat_id"], &upsert.kind)?;
    } else if upsert.kind == "outbound_message" {
        validate_outbound_message_payload(&upsert.payload)?;
        validate_target_fields(&upsert.target, &["channel", "chat_id"], "outbound_message")?;
    }

    Ok(upsert)
}

fn enrich_target_from_context(
    target: serde_json::Value,
    context: &crate::tools::ToolContext,
) -> serde_json::Value {
    let mut object = match target {
        serde_json::Value::Object(map) => map,
        _ => return target,
    };

    if !has_non_empty_string(&object, "channel") {
        if let Some(channel_name) = context
            .channel_name
            .as_ref()
            .filter(|value| !value.trim().is_empty())
        {
            object.insert(
                "channel".to_string(),
                serde_json::Value::String(channel_name.clone()),
            );
        }
    }

    if !has_non_empty_string(&object, "chat_id") {
        if let Some(chat_id) = context
            .chat_id
            .as_ref()
            .filter(|value| !value.trim().is_empty())
        {
            object.insert(
                "chat_id".to_string(),
                serde_json::Value::String(chat_id.clone()),
            );
        }
    }

    serde_json::Value::Object(object)
}

fn has_non_empty_string(object: &serde_json::Map<String, serde_json::Value>, field: &str) -> bool {
    object
        .get(field)
        .and_then(|value| value.as_str())
        .map(|value| !value.trim().is_empty())
        .unwrap_or(false)
}

fn validate_agent_task_payload(
    payload: &serde_json::Value,
    known_agents: &HashSet<String>,
) -> anyhow::Result<()> {
    let Some(prompt) = payload.get("prompt").and_then(|value| value.as_str()) else {
        anyhow::bail!("agent_task payload.prompt is required and must be a string")
    };
    if prompt.trim().is_empty() {
        anyhow::bail!("agent_task payload.prompt cannot be empty")
    }

    let Some(agent_name) = payload.get("agent").and_then(|value| value.as_str()) else {
        return Ok(());
    };

    let normalized = agent_name.trim();
    if normalized.is_empty() || normalized == "default" || known_agents.contains(normalized) {
        return Ok(());
    }

    anyhow::bail!(unknown_agent_message(normalized, known_agents))
}

fn unknown_agent_message(agent_name: &str, known_agents: &HashSet<String>) -> String {
    let mut configured_agents = known_agents.iter().cloned().collect::<Vec<_>>();
    configured_agents.sort();

    let configured_hint = if configured_agents.is_empty() {
        "No named agents are configured; use payload.agent='default' or omit payload.agent."
            .to_string()
    } else {
        format!(
            "payload.agent must be omitted, set to 'default', or use one of configured agents: default, {}.",
            configured_agents.join(", ")
        )
    };

    format!(
        "Unknown agent '{}' for agent_task payload.agent. {} '{}' is not an agent. If you mean a skill, do not put it in payload.agent.",
        agent_name, configured_hint, agent_name,
    )
}

fn validate_outbound_message_payload(payload: &serde_json::Value) -> anyhow::Result<()> {
    let Some(content) = payload.get("content").and_then(|value| value.as_str()) else {
        anyhow::bail!("outbound_message payload.content is required and must be a string")
    };
    if content.trim().is_empty() {
        anyhow::bail!("outbound_message payload.content cannot be empty")
    }
    Ok(())
}

fn validate_target_fields(
    target: &serde_json::Value,
    required_fields: &[&str],
    kind: &str,
) -> anyhow::Result<()> {
    let object = target
        .as_object()
        .ok_or_else(|| anyhow::anyhow!("{} target must be an object", kind))?;

    for field in required_fields {
        let Some(value) = object.get(*field).and_then(|value| value.as_str()) else {
            anyhow::bail!("{} target.{} is required and must be a string", kind, field)
        };
        if value.trim().is_empty() {
            anyhow::bail!("{} target.{} cannot be empty", kind, field)
        }
    }

    Ok(())
}

fn record_to_json(record: &SchedulerJobRecord) -> serde_json::Value {
    json!({
        "id": record.id,
        "kind": record.kind,
        "schedule": record.schedule,
        "target": record.target,
        "payload": record.payload,
        "enabled": record.enabled,
        "state": record.state,
        "last_status": record.last_status,
        "last_error": record.last_error,
        "run_count": record.run_count,
        "max_runs": record.max_runs,
        "last_fired_at": record.last_fired_at,
        "next_fire_at": record.next_fire_at,
        "paused_at": record.paused_at,
        "completed_at": record.completed_at,
        "created_at": record.created_at,
        "updated_at": record.updated_at,
    })
}

fn record_to_upsert(record: &SchedulerJobRecord) -> SchedulerJobUpsert {
    SchedulerJobUpsert {
        id: record.id.clone(),
        kind: record.kind.clone(),
        schedule: record.schedule.clone(),
        interval_secs: record.interval_secs,
        startup_delay_secs: record.startup_delay_secs,
        target: record.target.clone(),
        payload: record.payload.clone(),
        enabled: record.enabled,
        state: record.state.clone(),
        last_status: record.last_status.clone(),
        last_error: record.last_error.clone(),
        run_count: record.run_count,
        max_runs: record.max_runs,
        last_fired_at: record.last_fired_at,
        next_fire_at: record.next_fire_at,
        paused_at: record.paused_at,
        completed_at: record.completed_at,
    }
}

fn require_str<'a>(args: &'a serde_json::Value, key: &str) -> anyhow::Result<&'a str> {
    args.get(key)
        .and_then(|value| value.as_str())
        .ok_or_else(|| anyhow::anyhow!("Missing required parameter: {}", key))
}

fn error_result(message: &str) -> ToolResult {
    ToolResult {
        success: false,
        output: String::new(),
        error: Some(message.to_string()),
    }
}

fn current_timestamp() -> i64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap_or_default()
        .as_millis() as i64
}

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

    const TEST_CHANNEL: &str = "test-channel";

    #[tokio::test]
    async fn test_scheduler_manage_put_and_get() {
        let store = Arc::new(SessionStore::in_memory().unwrap());
        let tool = SchedulerManageTool::new(store, HashSet::new());

        let put_result = tool
            .execute(json!({
                "action": "put",
                "id": "heartbeat",
                "kind": "outbound_message",
                "schedule": {
                    "type": "interval",
                    "seconds": 60
                },
                "target": {
                    "channel": "test-channel",
                    "chat_id": "oc_demo"
                },
                "payload": {
                    "content": "ping"
                }
            }))
            .await
            .unwrap();
        assert!(put_result.success);

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

    #[tokio::test]
    async fn test_scheduler_manage_put_agent_task() {
        let store = Arc::new(SessionStore::in_memory().unwrap());
        let tool = SchedulerManageTool::new(store, HashSet::from(["planner".to_string()]));

        let put_result = tool
            .execute(json!({
                "action": "put",
                "id": "agent.daily_summary",
                "kind": "agent_task",
                "schedule": {
                    "type": "cron",
                    "expression": "0 9 * * *"
                },
                "target": {
                    "channel": "test-channel",
                    "chat_id": "oc_demo"
                },
                "payload": {
                    "prompt": "请总结今天待办",
                    "agent": "planner"
                }
            }))
            .await
            .unwrap();

        assert!(put_result.success);
        assert!(put_result.output.contains("agent_task"));
    }

    #[tokio::test]
    async fn test_scheduler_manage_put_silent_agent_task() {
        let store = Arc::new(SessionStore::in_memory().unwrap());
        let tool = SchedulerManageTool::new(store.clone(), HashSet::from(["planner".to_string()]));

        let put_result = tool
            .execute(json!({
                "action": "put",
                "id": "agent.daily_summary.background",
                "kind": "silent_agent_task",
                "schedule": {
                    "type": "cron",
                    "expression": "0 9 * * *"
                },
                "target": {
                    "channel": "test-channel",
                    "chat_id": "oc_demo",
                    "session_chat_id": "scheduler/agent.daily_summary.background"
                },
                "payload": {
                    "prompt": "请后台总结今天待办",
                    "agent": "planner"
                }
            }))
            .await
            .unwrap();

        assert!(put_result.success);

        let saved = store
            .get_scheduler_job("agent.daily_summary.background")
            .unwrap()
            .unwrap();
        assert_eq!(saved.kind, "silent_agent_task");
        assert_eq!(
            saved.target["session_chat_id"],
            "scheduler/agent.daily_summary.background"
        );
    }

    #[tokio::test]
    async fn test_scheduler_manage_rejects_outbound_message_without_target() {
        let store = Arc::new(SessionStore::in_memory().unwrap());
        let tool = SchedulerManageTool::new(store, HashSet::new());

        let put_result = tool
            .execute(json!({
                "action": "put",
                "id": "massage_reminder",
                "kind": "outbound_message",
                "schedule": {
                    "type": "interval",
                    "seconds": 60
                },
                "payload": {
                    "content": "⏰ 时间到了！该去按摩了！💆"
                }
            }))
            .await;

        assert!(put_result.is_err());
        let error = put_result.err().unwrap().to_string();
        assert!(error.contains("outbound_message target.channel is required"));
    }

    #[tokio::test]
    async fn test_scheduler_manage_put_uses_tool_context_target_defaults() {
        let store = Arc::new(SessionStore::in_memory().unwrap());
        let tool = SchedulerManageTool::new(store.clone(), HashSet::new());

        let put_result = tool
            .execute_with_context(
                &crate::tools::ToolContext {
                    channel_name: Some(TEST_CHANNEL.to_string()),
                    sender_id: Some("user-1".to_string()),
                    chat_id: Some("oc_demo".to_string()),
                    session_id: Some(format!("{}:oc_demo", TEST_CHANNEL)),
                    message_id: Some("msg-1".to_string()),
                    message_seq: Some(1),
                    ..Default::default()
                },
                json!({
                    "action": "put",
                    "id": "work_reminder",
                    "kind": "outbound_message",
                    "schedule": {
                        "type": "interval",
                        "seconds": 60
                    },
                    "payload": {
                        "content": "ping"
                    }
                }),
            )
            .await
            .unwrap();

        assert!(put_result.success);

        let saved = store.get_scheduler_job("work_reminder").unwrap().unwrap();
        assert_eq!(saved.target["channel"], "test-channel");
        assert_eq!(saved.target["chat_id"], "oc_demo");
    }

    #[tokio::test]
    async fn test_scheduler_manage_rejects_unknown_agent_task_agent() {
        let store = Arc::new(SessionStore::in_memory().unwrap());
        let tool = SchedulerManageTool::new(store, HashSet::from(["planner".to_string()]));

        let put_result = tool
            .execute(json!({
                "action": "put",
                "id": "agent.daily_summary",
                "kind": "agent_task",
                "schedule": {
                    "type": "cron",
                    "expression": "0 9 * * *"
                },
                "target": {
                    "channel": "test-channel",
                    "chat_id": "oc_demo"
                },
                "payload": {
                    "prompt": "请总结今天待办",
                    "agent": "missing-agent"
                }
            }))
            .await;

        assert!(put_result.is_err());
        let error = put_result.err().unwrap().to_string();
        assert!(error.contains("Unknown agent 'missing-agent' for agent_task payload.agent"));
        assert!(error.contains("payload.agent must be omitted, set to 'default', or use one of configured agents: default, planner"));
        assert!(error.contains("If you mean a skill, do not put it in payload.agent"));
    }

    #[tokio::test]
    async fn test_scheduler_manage_accepts_default_agent() {
        let store = Arc::new(SessionStore::in_memory().unwrap());
        let tool = SchedulerManageTool::new(store, HashSet::from(["planner".to_string()]));

        let put_result = tool
            .execute(json!({
                "action": "put",
                "id": "agent.default_summary",
                "kind": "agent_task",
                "schedule": {
                    "type": "cron",
                    "expression": "0 9 * * *"
                },
                "target": {
                    "channel": "test-channel",
                    "chat_id": "oc_demo"
                },
                "payload": {
                    "prompt": "请总结今天待办",
                    "agent": "default"
                }
            }))
            .await
            .unwrap();

        assert!(put_result.success);
    }

    #[tokio::test]
    async fn test_scheduler_manage_rejects_null_args_locally() {
        let store = Arc::new(SessionStore::in_memory().unwrap());
        let tool = SchedulerManageTool::new(store, HashSet::new());

        let result = tool.execute(serde_json::Value::Null).await.unwrap();

        assert!(!result.success);
        assert_eq!(
            result.error.as_deref(),
            Some(
                "Missing required parameters: scheduler_manage expects a JSON object like {\"action\":\"list\"}"
            )
        );
    }

    #[test]
    fn test_scheduler_manage_schema_advises_against_repeating_schedule_in_agent_task_prompt() {
        let store = Arc::new(SessionStore::in_memory().unwrap());
        let tool = SchedulerManageTool::new(store, HashSet::new());

        let schema = tool.parameters_schema();
        let payload_description = schema["properties"]["payload"]["description"]
            .as_str()
            .unwrap();

        assert!(
            payload_description.contains("avoid repeating schedule phrases or execution times")
        );
        assert!(payload_description.contains("每天9点"));
        assert!(payload_description.contains("每小时"));
    }

    #[tokio::test]
    async fn test_scheduler_manage_update_partial_fields() {
        let store = Arc::new(SessionStore::in_memory().unwrap());
        let tool = SchedulerManageTool::new(store.clone(), HashSet::new());

        // First, create a job
        let put_result = tool
            .execute(json!({
                "action": "put",
                "id": "test_update_job",
                "kind": "outbound_message",
                "schedule": {
                    "type": "interval",
                    "seconds": 60
                },
                "target": {
                    "channel": "test-channel",
                    "chat_id": "oc_demo"
                },
                "payload": {
                    "content": "original message"
                },
                "max_runs": 10
            }))
            .await
            .unwrap();
        assert!(put_result.success);

        // Update only payload
        let update_result = tool
            .execute(json!({
                "action": "update",
                "id": "test_update_job",
                "payload": {
                    "content": "updated message"
                }
            }))
            .await
            .unwrap();
        assert!(update_result.success);
        assert!(update_result.output.contains("updated message"));
        assert!(update_result.output.contains("test_update_job"));

        // Verify other fields preserved
        let get_result = tool
            .execute(json!({
                "action": "get",
                "id": "test_update_job"
            }))
            .await
            .unwrap();
        assert!(get_result.success);
        assert!(get_result.output.contains("interval"));
        assert!(get_result.output.contains("test-channel"));
        assert!(get_result.output.contains("max_runs\": 10"));
    }

    #[tokio::test]
    async fn test_scheduler_manage_update_schedule() {
        let store = Arc::new(SessionStore::in_memory().unwrap());
        let tool = SchedulerManageTool::new(store.clone(), HashSet::new());

        // Create job
        let _ = tool
            .execute(json!({
                "action": "put",
                "id": "test_update_schedule",
                "kind": "outbound_message",
                "schedule": {
                    "type": "interval",
                    "seconds": 60
                },
                "target": {
                    "channel": "test",
                    "chat_id": "oc_demo"
                },
                "payload": { "content": "ping" }
            }))
            .await
            .unwrap();

        // Update schedule
        let update_result = tool
            .execute(json!({
                "action": "update",
                "id": "test_update_schedule",
                "schedule": {
                    "type": "cron",
                    "expression": "0 9 * * *"
                }
            }))
            .await
            .unwrap();
        assert!(update_result.success);
        assert!(update_result.output.contains("cron"));
        assert!(update_result.output.contains("0 9 * * *"));
    }

    #[tokio::test]
    async fn test_scheduler_manage_update_enabled() {
        let store = Arc::new(SessionStore::in_memory().unwrap());
        let tool = SchedulerManageTool::new(store.clone(), HashSet::new());

        // Create enabled job
        let _ = tool
            .execute(json!({
                "action": "put",
                "id": "test_update_enabled",
                "kind": "outbound_message",
                "schedule": { "type": "interval", "seconds": 60 },
                "target": { "channel": "test", "chat_id": "oc_demo" },
                "payload": { "content": "ping" },
                "enabled": true
            }))
            .await
            .unwrap();

        // Disable it
        let update_result = tool
            .execute(json!({
                "action": "update",
                "id": "test_update_enabled",
                "enabled": false
            }))
            .await
            .unwrap();
        assert!(update_result.success);
        assert!(update_result.output.contains("\"enabled\": false"));
        assert!(update_result.output.contains("paused"));

        // Re-enable it
        let update_result = tool
            .execute(json!({
                "action": "update",
                "id": "test_update_enabled",
                "enabled": true
            }))
            .await
            .unwrap();
        assert!(update_result.success);
        assert!(update_result.output.contains("\"enabled\": true"));
        assert!(update_result.output.contains("scheduled"));
    }

    #[tokio::test]
    async fn test_scheduler_manage_update_job_not_found() {
        let store = Arc::new(SessionStore::in_memory().unwrap());
        let tool = SchedulerManageTool::new(store, HashSet::new());

        let result = tool
            .execute(json!({
                "action": "update",
                "id": "nonexistent_job",
                "payload": { "content": "new" }
            }))
            .await;

        assert!(result.is_err());
        let error = result.err().unwrap().to_string();
        assert!(error.contains("scheduler job 'nonexistent_job' not found"));
    }

    #[tokio::test]
    async fn test_scheduler_manage_update_preserves_agent_task_agent() {
        let store = Arc::new(SessionStore::in_memory().unwrap());
        let tool = SchedulerManageTool::new(store.clone(), HashSet::from(["planner".to_string()]));

        // Create agent_task
        let _ = tool
            .execute(json!({
                "action": "put",
                "id": "test_update_agent",
                "kind": "agent_task",
                "schedule": { "type": "cron", "expression": "0 9 * * *" },
                "target": { "channel": "test", "chat_id": "oc_demo" },
                "payload": {
                    "prompt": "original task",
                    "agent": "planner"
                }
            }))
            .await
            .unwrap();

        // Update only prompt
        let update_result = tool
            .execute(json!({
                "action": "update",
                "id": "test_update_agent",
                "payload": {
                    "prompt": "updated task",
                    "agent": "planner"
                }
            }))
            .await
            .unwrap();
        assert!(update_result.success);

        // Verify agent preserved (when explicitly provided)
        let get_result = tool
            .execute(json!({
                "action": "get",
                "id": "test_update_agent"
            }))
            .await
            .unwrap();
        assert!(get_result.output.contains("planner"));
        assert!(get_result.output.contains("updated task"));
    }
}
