use crate::agents::PipelineStage;
use crate::config::ProjectConfig;
use crate::http::context::AppContext;
use crate::http::settings::get_editor_command_from_store;
use crate::slog_warn;
use crate::worktree;
use serde_json::{json, Value};

pub(super) async fn tool_start_agent(args: &Value, ctx: &AppContext) -> Result<String, String> {
    let story_id = args
        .get("story_id")
        .and_then(|v| v.as_str())
        .ok_or("Missing required argument: story_id")?;
    let agent_name = args.get("agent_name").and_then(|v| v.as_str());

    let project_root = ctx.agents.get_project_root(&ctx.state)?;
    let info = ctx
        .agents
        .start_agent(&project_root, story_id, agent_name, None)
        .await?;

    // Snapshot coverage baseline from the most recent coverage report (best-effort).
    if let Some(pct) = read_coverage_percent_from_json(&project_root)
        && let Err(e) = crate::http::workflow::write_coverage_baseline_to_story_file(
            &project_root,
            story_id,
            pct,
        )
    {
        slog_warn!("[start_agent] Could not write coverage baseline to story file: {e}");
    }

    serde_json::to_string_pretty(&json!({
        "story_id": info.story_id,
        "agent_name": info.agent_name,
        "status": info.status.to_string(),
        "session_id": info.session_id,
        "worktree_path": info.worktree_path,
    }))
    .map_err(|e| format!("Serialization error: {e}"))
}

/// Try to read the overall line coverage percentage from the llvm-cov JSON report.
///
/// Expects the file at `{project_root}/.storkit/coverage/server.json`.
/// Returns `None` if the file is absent, unreadable, or cannot be parsed.
pub(super) fn read_coverage_percent_from_json(project_root: &std::path::Path) -> Option<f64> {
    let path = project_root
        .join(".storkit")
        .join("coverage")
        .join("server.json");
    let contents = std::fs::read_to_string(&path).ok()?;
    let json: Value = serde_json::from_str(&contents).ok()?;
    // cargo llvm-cov --json format: data[0].totals.lines.percent
    json.pointer("/data/0/totals/lines/percent")
        .and_then(|v| v.as_f64())
}

pub(super) async fn tool_stop_agent(args: &Value, ctx: &AppContext) -> Result<String, String> {
    let story_id = args
        .get("story_id")
        .and_then(|v| v.as_str())
        .ok_or("Missing required argument: story_id")?;
    let agent_name = args
        .get("agent_name")
        .and_then(|v| v.as_str())
        .ok_or("Missing required argument: agent_name")?;

    let project_root = ctx.agents.get_project_root(&ctx.state)?;
    ctx.agents
        .stop_agent(&project_root, story_id, agent_name)
        .await?;

    Ok(format!("Agent '{agent_name}' for story '{story_id}' stopped."))
}

pub(super) fn tool_list_agents(ctx: &AppContext) -> Result<String, String> {
    let project_root = ctx.agents.get_project_root(&ctx.state).ok();
    let agents = ctx.agents.list_agents()?;
    serde_json::to_string_pretty(&json!(agents
        .iter()
        .filter(|a| {
            project_root
                .as_deref()
                .map(|root| !crate::http::agents::story_is_archived(root, &a.story_id))
                .unwrap_or(true)
        })
        .map(|a| json!({
            "story_id": a.story_id,
            "agent_name": a.agent_name,
            "status": a.status.to_string(),
            "session_id": a.session_id,
            "worktree_path": a.worktree_path,
        }))
        .collect::<Vec<_>>()))
    .map_err(|e| format!("Serialization error: {e}"))
}

pub(super) async fn tool_get_agent_output_poll(args: &Value, ctx: &AppContext) -> Result<String, String> {
    let story_id = args
        .get("story_id")
        .and_then(|v| v.as_str())
        .ok_or("Missing required argument: story_id")?;
    let agent_name = args
        .get("agent_name")
        .and_then(|v| v.as_str())
        .ok_or("Missing required argument: agent_name")?;

    // Try draining in-memory events first.
    match ctx.agents.drain_events(story_id, agent_name) {
        Ok(drained) => {
            let done = drained.iter().any(|e| {
                matches!(
                    e,
                    crate::agents::AgentEvent::Done { .. }
                        | crate::agents::AgentEvent::Error { .. }
                )
            });

            let events: Vec<serde_json::Value> = drained
                .into_iter()
                .filter_map(|e| serde_json::to_value(&e).ok())
                .collect();

            serde_json::to_string_pretty(&json!({
                "events": events,
                "done": done,
                "event_count": events.len(),
                "message": if done { "Agent stream ended." } else if events.is_empty() { "No new events. Call again to continue." } else { "Events returned. Call again to continue." }
            }))
            .map_err(|e| format!("Serialization error: {e}"))
        }
        Err(_) => {
            // Agent not in memory — fall back to persistent log file.
            get_agent_output_from_log(story_id, agent_name, ctx)
        }
    }
}

/// Fall back to reading agent output from the persistent log file on disk.
///
/// Tries to find the log file via the agent's stored log_session_id first,
/// then falls back to `find_latest_log` scanning the log directory.
pub(super) fn get_agent_output_from_log(
    story_id: &str,
    agent_name: &str,
    ctx: &AppContext,
) -> Result<String, String> {
    use crate::agent_log;

    let project_root = ctx.agents.get_project_root(&ctx.state)?;

    // Try to find the log file: first from in-memory agent info, then by scanning.
    let log_path = ctx
        .agents
        .get_log_info(story_id, agent_name)
        .map(|(session_id, root)| agent_log::log_file_path(&root, story_id, agent_name, &session_id))
        .filter(|p| p.exists())
        .or_else(|| agent_log::find_latest_log(&project_root, story_id, agent_name));

    let log_path = match log_path {
        Some(p) => p,
        None => {
            return serde_json::to_string_pretty(&json!({
                "events": [],
                "done": true,
                "event_count": 0,
                "message": format!("No agent '{agent_name}' for story '{story_id}' and no log file found."),
                "source": "none",
            }))
            .map_err(|e| format!("Serialization error: {e}"));
        }
    };

    match agent_log::read_log(&log_path) {
        Ok(entries) => {
            let events: Vec<serde_json::Value> = entries
                .into_iter()
                .map(|e| {
                    let mut val = e.event;
                    if let serde_json::Value::Object(ref mut map) = val {
                        map.insert(
                            "timestamp".to_string(),
                            serde_json::Value::String(e.timestamp),
                        );
                    }
                    val
                })
                .collect();

            let count = events.len();
            serde_json::to_string_pretty(&json!({
                "events": events,
                "done": true,
                "event_count": count,
                "message": "Events loaded from persistent log file.",
                "source": "log_file",
                "log_file": log_path.to_string_lossy(),
            }))
            .map_err(|e| format!("Serialization error: {e}"))
        }
        Err(e) => Err(format!("Failed to read log file: {e}")),
    }
}

pub(super) fn tool_get_agent_config(ctx: &AppContext) -> Result<String, String> {
    let project_root = ctx.agents.get_project_root(&ctx.state)?;
    let config = ProjectConfig::load(&project_root)?;

    // Collect available (idle) agent names across all stages so the caller can
    // see at a glance which agents are free to start (story 190).
    let mut available_names: std::collections::HashSet<String> =
        std::collections::HashSet::new();
    for stage in &[
        PipelineStage::Coder,
        PipelineStage::Qa,
        PipelineStage::Mergemaster,
        PipelineStage::Other,
    ] {
        if let Ok(names) = ctx.agents.available_agents_for_stage(&config, stage) {
            available_names.extend(names);
        }
    }

    serde_json::to_string_pretty(&json!(config
        .agent
        .iter()
        .map(|a| json!({
            "name": a.name,
            "role": a.role,
            "model": a.model,
            "allowed_tools": a.allowed_tools,
            "max_turns": a.max_turns,
            "max_budget_usd": a.max_budget_usd,
            "available": available_names.contains(&a.name),
        }))
        .collect::<Vec<_>>()))
    .map_err(|e| format!("Serialization error: {e}"))
}

pub(super) async fn tool_wait_for_agent(args: &Value, ctx: &AppContext) -> Result<String, String> {
    let story_id = args
        .get("story_id")
        .and_then(|v| v.as_str())
        .ok_or("Missing required argument: story_id")?;
    let agent_name = args
        .get("agent_name")
        .and_then(|v| v.as_str())
        .ok_or("Missing required argument: agent_name")?;
    let timeout_ms = args
        .get("timeout_ms")
        .and_then(|v| v.as_u64())
        .unwrap_or(300_000); // default: 5 minutes

    let info = ctx
        .agents
        .wait_for_agent(story_id, agent_name, timeout_ms)
        .await?;

    let commits = match (&info.worktree_path, &info.base_branch) {
        (Some(wt_path), Some(base)) => get_worktree_commits(wt_path, base).await,
        _ => None,
    };

    let completion = info.completion.as_ref().map(|r| json!({
        "summary": r.summary,
        "gates_passed": r.gates_passed,
        "gate_output": r.gate_output,
    }));

    serde_json::to_string_pretty(&json!({
        "story_id": info.story_id,
        "agent_name": info.agent_name,
        "status": info.status.to_string(),
        "session_id": info.session_id,
        "worktree_path": info.worktree_path,
        "base_branch": info.base_branch,
        "commits": commits,
        "completion": completion,
    }))
    .map_err(|e| format!("Serialization error: {e}"))
}

pub(super) async fn tool_create_worktree(args: &Value, ctx: &AppContext) -> Result<String, String> {
    let story_id = args
        .get("story_id")
        .and_then(|v| v.as_str())
        .ok_or("Missing required argument: story_id")?;

    let project_root = ctx.agents.get_project_root(&ctx.state)?;
    let info = ctx.agents.create_worktree(&project_root, story_id).await?;

    serde_json::to_string_pretty(&json!({
        "story_id": story_id,
        "worktree_path": info.path.to_string_lossy(),
        "branch": info.branch,
        "base_branch": info.base_branch,
    }))
    .map_err(|e| format!("Serialization error: {e}"))
}

pub(super) fn tool_list_worktrees(ctx: &AppContext) -> Result<String, String> {
    let project_root = ctx.agents.get_project_root(&ctx.state)?;
    let entries = worktree::list_worktrees(&project_root)?;

    serde_json::to_string_pretty(&json!(entries
        .iter()
        .map(|e| json!({
            "story_id": e.story_id,
            "path": e.path.to_string_lossy(),
        }))
        .collect::<Vec<_>>()))
    .map_err(|e| format!("Serialization error: {e}"))
}

pub(super) async fn tool_remove_worktree(args: &Value, ctx: &AppContext) -> Result<String, String> {
    let story_id = args
        .get("story_id")
        .and_then(|v| v.as_str())
        .ok_or("Missing required argument: story_id")?;

    let project_root = ctx.agents.get_project_root(&ctx.state)?;
    let config = ProjectConfig::load(&project_root)?;
    worktree::remove_worktree_by_story_id(&project_root, story_id, &config).await?;

    Ok(format!("Worktree for story '{story_id}' removed."))
}

pub(super) fn tool_get_editor_command(args: &Value, ctx: &AppContext) -> Result<String, String> {
    let worktree_path = args
        .get("worktree_path")
        .and_then(|v| v.as_str())
        .ok_or("Missing required argument: worktree_path")?;

    let editor = get_editor_command_from_store(ctx)
        .ok_or_else(|| "No editor configured. Set one via PUT /api/settings/editor.".to_string())?;

    Ok(format!("{editor} {worktree_path}"))
}

/// Run `git log <base>..HEAD --oneline` in the worktree and return the commit
/// summaries, or `None` if git is unavailable or there are no new commits.
pub(super) async fn get_worktree_commits(worktree_path: &str, base_branch: &str) -> Option<Vec<String>> {
    let wt = worktree_path.to_string();
    let base = base_branch.to_string();
    tokio::task::spawn_blocking(move || {
        let output = std::process::Command::new("git")
            .args(["log", &format!("{base}..HEAD"), "--oneline"])
            .current_dir(&wt)
            .output()
            .ok()?;

        if output.status.success() {
            let lines: Vec<String> = String::from_utf8(output.stdout)
                .ok()?
                .lines()
                .filter(|l| !l.is_empty())
                .map(|l| l.to_string())
                .collect();
            Some(lines)
        } else {
            None
        }
    })
    .await
    .ok()
    .flatten()
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::http::context::AppContext;
    use crate::store::StoreOps;

    fn test_ctx(dir: &std::path::Path) -> AppContext {
        AppContext::new_test(dir.to_path_buf())
    }

    #[test]
    fn tool_list_agents_empty() {
        let tmp = tempfile::tempdir().unwrap();
        let ctx = test_ctx(tmp.path());
        let result = tool_list_agents(&ctx).unwrap();
        let parsed: Vec<Value> = serde_json::from_str(&result).unwrap();
        assert!(parsed.is_empty());
    }

    #[test]
    fn tool_get_agent_config_no_project_toml_returns_default_agent() {
        let tmp = tempfile::tempdir().unwrap();
        let ctx = test_ctx(tmp.path());
        // No project.toml → default config with one fallback agent
        let result = tool_get_agent_config(&ctx).unwrap();
        let parsed: Vec<Value> = serde_json::from_str(&result).unwrap();
        // Default config contains one agent entry with default values
        assert_eq!(parsed.len(), 1, "default config should have one fallback agent");
        assert!(parsed[0].get("name").is_some());
        assert!(parsed[0].get("role").is_some());
    }

    #[tokio::test]
    async fn tool_get_agent_output_poll_missing_story_id() {
        let tmp = tempfile::tempdir().unwrap();
        let ctx = test_ctx(tmp.path());
        let result = tool_get_agent_output_poll(&json!({"agent_name": "bot"}), &ctx).await;
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("story_id"));
    }

    #[tokio::test]
    async fn tool_get_agent_output_poll_missing_agent_name() {
        let tmp = tempfile::tempdir().unwrap();
        let ctx = test_ctx(tmp.path());
        let result =
            tool_get_agent_output_poll(&json!({"story_id": "1_test"}), &ctx).await;
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("agent_name"));
    }

    #[tokio::test]
    async fn tool_get_agent_output_poll_no_agent_falls_back_to_empty_log() {
        let tmp = tempfile::tempdir().unwrap();
        let ctx = test_ctx(tmp.path());
        // No agent registered, no log file → returns empty response from log fallback
        let result = tool_get_agent_output_poll(
            &json!({"story_id": "99_nope", "agent_name": "bot"}),
            &ctx,
        )
        .await
        .unwrap();
        let parsed: Value = serde_json::from_str(&result).unwrap();
        assert_eq!(parsed["done"], true);
        assert_eq!(parsed["event_count"], 0);
        assert!(
            parsed["message"].as_str().unwrap_or("").contains("No agent"),
            "expected 'No agent' message: {parsed}"
        );
    }

    #[tokio::test]
    async fn tool_get_agent_output_poll_with_running_agent_returns_empty_events() {
        let tmp = tempfile::tempdir().unwrap();
        let ctx = test_ctx(tmp.path());
        // Inject a running agent — no events broadcast yet
        ctx.agents
            .inject_test_agent("10_story", "worker", crate::agents::AgentStatus::Running);
        let result = tool_get_agent_output_poll(
            &json!({"story_id": "10_story", "agent_name": "worker"}),
            &ctx,
        )
        .await
        .unwrap();
        let parsed: Value = serde_json::from_str(&result).unwrap();
        assert_eq!(parsed["done"], false);
        assert_eq!(parsed["event_count"], 0);
        assert!(parsed["events"].is_array());
    }

    #[tokio::test]
    async fn tool_stop_agent_missing_story_id() {
        let tmp = tempfile::tempdir().unwrap();
        let ctx = test_ctx(tmp.path());
        let result = tool_stop_agent(&json!({"agent_name": "bot"}), &ctx).await;
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("story_id"));
    }

    #[tokio::test]
    async fn tool_stop_agent_missing_agent_name() {
        let tmp = tempfile::tempdir().unwrap();
        let ctx = test_ctx(tmp.path());
        let result = tool_stop_agent(&json!({"story_id": "1_test"}), &ctx).await;
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("agent_name"));
    }

    #[tokio::test]
    async fn tool_start_agent_missing_story_id() {
        let tmp = tempfile::tempdir().unwrap();
        let ctx = test_ctx(tmp.path());
        let result = tool_start_agent(&json!({}), &ctx).await;
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("story_id"));
    }

    #[tokio::test]
    async fn tool_start_agent_no_agent_name_no_coder_returns_clear_error() {
        // Config has only a supervisor — start_agent without agent_name should
        // refuse rather than silently assigning supervisor.
        let tmp = tempfile::tempdir().unwrap();
        let sk = tmp.path().join(".storkit");
        std::fs::create_dir_all(&sk).unwrap();
        std::fs::write(
            sk.join("project.toml"),
            r#"
[[agent]]
name = "supervisor"
stage = "other"
"#,
        )
        .unwrap();
        let ctx = test_ctx(tmp.path());
        let result = tool_start_agent(&json!({"story_id": "42_my_story"}), &ctx).await;
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert!(
            err.contains("coder"),
            "error should mention 'coder', got: {err}"
        );
    }

    #[tokio::test]
    async fn tool_start_agent_no_agent_name_picks_coder_not_supervisor() {
        // Config has supervisor first, then coder-1. Without agent_name the
        // coder should be selected, not supervisor. The call will fail due to
        // missing git repo / worktree, but the error must NOT be about
        // "No coder agent configured".
        let tmp = tempfile::tempdir().unwrap();
        let sk = tmp.path().join(".storkit");
        std::fs::create_dir_all(&sk).unwrap();
        std::fs::write(
            sk.join("project.toml"),
            r#"
[[agent]]
name = "supervisor"
stage = "other"

[[agent]]
name = "coder-1"
stage = "coder"
"#,
        )
        .unwrap();
        let ctx = test_ctx(tmp.path());
        let result = tool_start_agent(&json!({"story_id": "42_my_story"}), &ctx).await;
        // May succeed or fail for infrastructure reasons (no git repo), but
        // must NOT fail with "No coder agent configured".
        if let Err(err) = result {
            assert!(
                !err.contains("No coder agent configured"),
                "should not fail on agent selection, got: {err}"
            );
            // Should also not complain about supervisor being absent.
            assert!(
                !err.contains("supervisor"),
                "should not select supervisor, got: {err}"
            );
        }
    }

    #[tokio::test]
    async fn tool_create_worktree_missing_story_id() {
        let tmp = tempfile::tempdir().unwrap();
        let ctx = test_ctx(tmp.path());
        let result = tool_create_worktree(&json!({}), &ctx).await;
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("story_id"));
    }

    #[tokio::test]
    async fn tool_remove_worktree_missing_story_id() {
        let tmp = tempfile::tempdir().unwrap();
        let ctx = test_ctx(tmp.path());
        let result = tool_remove_worktree(&json!({}), &ctx).await;
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("story_id"));
    }

    #[test]
    fn tool_list_worktrees_empty_dir() {
        let tmp = tempfile::tempdir().unwrap();
        let ctx = test_ctx(tmp.path());
        let result = tool_list_worktrees(&ctx).unwrap();
        let parsed: Vec<Value> = serde_json::from_str(&result).unwrap();
        assert!(parsed.is_empty());
    }

    // ── Editor command tool tests ─────────────────────────────────

    #[test]
    fn tool_get_editor_command_missing_worktree_path() {
        let tmp = tempfile::tempdir().unwrap();
        let ctx = test_ctx(tmp.path());
        let result = tool_get_editor_command(&json!({}), &ctx);
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("worktree_path"));
    }

    #[test]
    fn tool_get_editor_command_no_editor_configured() {
        let tmp = tempfile::tempdir().unwrap();
        let ctx = test_ctx(tmp.path());
        let result = tool_get_editor_command(
            &json!({"worktree_path": "/some/path"}),
            &ctx,
        );
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("No editor configured"));
    }

    #[test]
    fn tool_get_editor_command_formats_correctly() {
        let tmp = tempfile::tempdir().unwrap();
        let ctx = test_ctx(tmp.path());
        ctx.store.set("editor_command", json!("zed"));

        let result = tool_get_editor_command(
            &json!({"worktree_path": "/home/user/worktrees/37_my_story"}),
            &ctx,
        )
        .unwrap();
        assert_eq!(result, "zed /home/user/worktrees/37_my_story");
    }

    #[test]
    fn tool_get_editor_command_works_with_vscode() {
        let tmp = tempfile::tempdir().unwrap();
        let ctx = test_ctx(tmp.path());
        ctx.store.set("editor_command", json!("code"));

        let result = tool_get_editor_command(
            &json!({"worktree_path": "/path/to/worktree"}),
            &ctx,
        )
        .unwrap();
        assert_eq!(result, "code /path/to/worktree");
    }

    #[test]
    fn get_editor_command_in_tools_list() {
        use super::super::{handle_tools_list};
        let resp = handle_tools_list(Some(json!(1)));
        let tools = resp.result.unwrap()["tools"].as_array().unwrap().clone();
        let tool = tools.iter().find(|t| t["name"] == "get_editor_command");
        assert!(tool.is_some(), "get_editor_command missing from tools list");
        let t = tool.unwrap();
        assert!(t["description"].is_string());
        let required = t["inputSchema"]["required"].as_array().unwrap();
        let req_names: Vec<&str> = required.iter().map(|v| v.as_str().unwrap()).collect();
        assert!(req_names.contains(&"worktree_path"));
    }

    #[tokio::test]
    async fn wait_for_agent_tool_missing_story_id() {
        let tmp = tempfile::tempdir().unwrap();
        let ctx = test_ctx(tmp.path());
        let result = tool_wait_for_agent(&json!({"agent_name": "bot"}), &ctx).await;
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("story_id"));
    }

    #[tokio::test]
    async fn wait_for_agent_tool_missing_agent_name() {
        let tmp = tempfile::tempdir().unwrap();
        let ctx = test_ctx(tmp.path());
        let result = tool_wait_for_agent(&json!({"story_id": "1_test"}), &ctx).await;
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("agent_name"));
    }

    #[tokio::test]
    async fn wait_for_agent_tool_nonexistent_agent_returns_error() {
        let tmp = tempfile::tempdir().unwrap();
        let ctx = test_ctx(tmp.path());
        let result =
            tool_wait_for_agent(&json!({"story_id": "99_nope", "agent_name": "bot", "timeout_ms": 50}), &ctx)
                .await;
        // No agent registered — should error
        assert!(result.is_err());
    }

    #[tokio::test]
    async fn wait_for_agent_tool_returns_completed_agent() {
        use crate::agents::AgentStatus;
        let tmp = tempfile::tempdir().unwrap();
        let ctx = test_ctx(tmp.path());
        ctx.agents
            .inject_test_agent("41_story", "worker", AgentStatus::Completed);

        let result = tool_wait_for_agent(
            &json!({"story_id": "41_story", "agent_name": "worker"}),
            &ctx,
        )
        .await
        .unwrap();
        let parsed: Value = serde_json::from_str(&result).unwrap();
        assert_eq!(parsed["status"], "completed");
        assert_eq!(parsed["story_id"], "41_story");
        assert_eq!(parsed["agent_name"], "worker");
        // commits key present (may be null since no real worktree)
        assert!(parsed.get("commits").is_some());
        // completion key present (null for agents that didn't call report_completion)
        assert!(parsed.get("completion").is_some());
    }

    #[test]
    fn wait_for_agent_tool_in_list() {
        use super::super::{handle_tools_list};
        let resp = handle_tools_list(Some(json!(1)));
        let tools = resp.result.unwrap()["tools"].as_array().unwrap().clone();
        let wait_tool = tools.iter().find(|t| t["name"] == "wait_for_agent");
        assert!(wait_tool.is_some(), "wait_for_agent missing from tools list");
        let t = wait_tool.unwrap();
        assert!(t["description"].as_str().unwrap().contains("block") || t["description"].as_str().unwrap().contains("Block"));
        let required = t["inputSchema"]["required"].as_array().unwrap();
        let req_names: Vec<&str> = required.iter().map(|v| v.as_str().unwrap()).collect();
        assert!(req_names.contains(&"story_id"));
        assert!(req_names.contains(&"agent_name"));
    }

    #[test]
    fn read_coverage_percent_from_json_parses_llvm_cov_format() {
        use std::fs;
        let tmp = tempfile::tempdir().unwrap();
        let cov_dir = tmp.path().join(".storkit/coverage");
        fs::create_dir_all(&cov_dir).unwrap();
        let json_content = r#"{"data":[{"totals":{"lines":{"count":100,"covered":78,"percent":78.0}}}]}"#;
        fs::write(cov_dir.join("server.json"), json_content).unwrap();

        let pct = read_coverage_percent_from_json(tmp.path());
        assert_eq!(pct, Some(78.0));
    }

    #[test]
    fn read_coverage_percent_from_json_returns_none_when_absent() {
        let tmp = tempfile::tempdir().unwrap();
        let pct = read_coverage_percent_from_json(tmp.path());
        assert!(pct.is_none());
    }
}
