//! `mcptest init` scaffolds a new mcptest project.
//!
//! Drops a working `tests/example.yaml`, a `tests/example-server.yml` mock
//! catalog, and a top-level `mcptest.yml` into the current directory. The
//! `mcptest.yml` is a schema-valid suite root mirroring the example, so the
//! advertised init-then-bare-run first contact passes offline (WOR-1352).
//! Existing files are refused unless `--force` is passed so a stray `init`
//! does not silently clobber a user's test suite.
//!
//!  added the URL target fork: when `--url` is supplied the scaffolder
//! emits a `server:` block keyed on `url:` plus the chosen auth recipe, and
//! the example test stops referencing the stdio filesystem server.

use std::fs;
use std::path::{Path, PathBuf};
use std::time::Duration;

use anyhow::{Context, Result};

/// Authentication recipe chosen by the URL fork of `mcptest init`.
///
/// Four variants cover the recipes called out in `docs/url-targets.md`,
/// matching open dev servers, a static bearer token from an env var, the
/// OAuth 2.1 PKCE flow `mcptest login` drives, and a single custom header
/// such as `X-API-Key` or a Cloudflare Access service token.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum InitAuth {
    /// No authentication. The server accepts open requests.
    None,
    /// Static bearer token read from an environment variable at run time.
    Bearer {
        /// Env var name that holds the token, for example `MCPTEST_API_TOKEN`.
        env_var: String,
    },
    /// OAuth 2.1 authorization-code flow with PKCE. The user authenticates
    /// once via `mcptest login`; the cached token is reused on later runs.
    OAuth2,
    /// Single custom header. `env_var` is set when the value should be
    /// read from the environment rather than baked into the YAML.
    CustomHeader {
        /// Header name (for example `CF-Access-Client-Id`).
        name: String,
        /// Env var that holds the header value, when one applies.
        env_var: Option<String>,
    },
}

/// URL-target scaffolding options collected by the init wizard.
///
/// The CLI plumbs these straight from flags into [`run_init`]. The shape
/// stays public so a future Linear ticket can swap in an interactive
/// prompt without churning the call signature.
#[derive(Debug, Clone)]
pub struct UrlTarget {
    /// Base URL the runner connects to (for example
    /// `https://mcp.example.com/v1`).
    pub url: String,
    /// Auth recipe chosen by the wizard or its `--auth-*` flag equivalents.
    pub auth: InitAuth,
    /// Optional readiness budget. When set, the YAML emits
    /// `wait_for_ready: <duration>` so the runner polls the server until
    /// `initialize` succeeds.
    pub wait_for_ready: Option<Duration>,
}

/// A discovered local MCP server to scaffold a suite from.
///
/// Built by the `init` handler from a [`mcptest_core::discovery::DiscoveredServer`]
/// the discovery walk found in a client config. Carries the launch command and
/// args verbatim (they came from the user's own machine), plus the env-var
/// *names* the server declares. The names are surfaced as a comment so the user
/// knows what to set; their values are never copied into the scaffold.
#[derive(Debug, Clone)]
pub struct DiscoveredTarget {
    /// Server key as it appeared in the client config.
    pub name: String,
    /// Executable the client launches.
    pub command: String,
    /// Arguments passed to the executable.
    pub args: Vec<String>,
    /// Env-var names the server declares (values are intentionally dropped).
    pub env_names: Vec<String>,
}

/// Build the YAML body for a discovered-server example.
///
/// Emits a stdio `servers:` block with the discovered command and a `tools/list`
/// probe test. Env-var names ride in a comment so the user sets them in their
/// environment; the scaffold never inlines a secret value.
pub fn scaffold_discovered_example(target: &DiscoveredTarget) -> String {
    let mut command_line = vec![target.command.clone()];
    command_line.extend(target.args.iter().cloned());
    // JSON encodes a string array as a valid YAML flow sequence with correct
    // quoting, sidestepping hand-rolled escaping of paths and flags.
    let command_yaml = serde_json::to_string(&command_line).unwrap_or_else(|_| "[]".to_string());

    let mut out = String::new();
    out.push_str("# yaml-language-server: $schema=https://mcptest.sh/schema/v1.json\n");
    out.push_str("#\n");
    out.push_str(&format!(
        "# mcptest suite scaffolded by `mcptest init --from-discovered {}`.\n",
        target.name
    ));
    out.push_str("# The server command was read from your local MCP client config.\n\n");
    out.push_str("servers:\n");
    out.push_str(&format!("  {}:\n", target.name));
    out.push_str(&format!("    command: {command_yaml}\n"));
    if !target.env_names.is_empty() {
        out.push_str(
            "    # This server declares these environment variables in your client config.\n",
        );
        out.push_str(
            "    # Set them in your shell before running; mcptest never copies their values here:\n",
        );
        for name in &target.env_names {
            out.push_str(&format!("    #   {name}\n"));
        }
    }
    out.push('\n');
    out.push_str("tools:\n");
    out.push_str("  - name: \"server advertises a tools list without error\"\n");
    out.push_str(&format!("    server: {}\n", target.name));
    out.push_str("    tool: \"__mcptest_probe__\"\n");
    out.push_str("    args: {}\n");
    out.push_str("    expect:\n");
    out.push_str("      - target: \"error\"\n");
    out.push_str("        matcher:\n");
    out.push_str("          exact: null\n");
    out.push_str("        message: \"probe call should not return a transport error\"\n");
    out
}

/// File header for the scaffolded `tests/example.yaml` (stdio fork).
///
/// Kept separate from [`EXAMPLE_SUITE_BODY`] so `mcptest.yml` can reuse the
/// same suite body under its own header (WOR-1352): a bare `mcptest run`
/// resolves `./mcptest.yml`, so that file must be a runnable suite, not a
/// settings-only scaffold the v1 schema rejects.
const EXAMPLE_HEADER: &str = r#"# yaml-language-server: $schema=https://mcptest.sh/schema/v1.json
#
# Hello-world mcptest suite scaffolded by `mcptest init`.
#
# This file is a standalone copy of the starter suite in ./mcptest.yml.
# A bare `mcptest run` executes mcptest.yml; run this file directly with
# `mcptest run tests/example.yaml`. Copy it when you add more suite files.

"#;

/// Shared suite body (servers plus one tool test) for the stdio fork.
///
/// Targets `mcptest mock` with the scaffolded `tests/example-server.yml`
/// catalog so the very first `mcptest run` passes offline, with no `npx`
/// download and no network. The comment shows how to swap in a real server.
const EXAMPLE_SUITE_BODY: &str = r#"servers:
  example:
    # `mcptest mock` ships with mcptest and serves the static catalog in
    # tests/example-server.yml, so the first run works offline. The path is
    # relative to the directory you run `mcptest` from (the project root).
    # Swap the command for your real MCP server when you are ready, e.g.:
    #   command: ["npx", "-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
    command: ["mcptest", "mock", "--tools-from", "tests/example-server.yml"]

tools:
  - name: "say_hello greets the caller"
    server: example
    tool: "say_hello"
    args:
      name: "mcptest"
    expect:
      - target: "result.content"
        matcher:
          schema:
            type: array
            minItems: 1
        message: "say_hello should return at least one content block"
"#;

/// Scaffolded `tests/example-server.yml`: the static catalog `mcptest mock`
/// serves for the starter suite.
///
/// Exists so first contact is hermetic: the scaffolded suites pass with no
/// network and no real server. Deleted by the user once the suite targets
/// their own server.
const EXAMPLE_MOCK_MANIFEST: &str = r#"# Static tool catalog served by `mcptest mock` for the scaffolded example
# suite. `mcptest mock --tools-from tests/example-server.yml` speaks MCP over
# stdio and answers from this file, so the first run needs no real server.
# Delete this file once the suite targets your own server.
mock_server:
  name: example
  tools:
    - name: say_hello
      description: "Echo a greeting back to the caller."
      input_schema:
        type: object
        properties:
          name:
            type: string
        required: [name]
      response:
        content:
          - type: text
            text: "Hello, ${args.name}!"
"#;

/// File header for the scaffolded `mcptest.yml` project suite.
///
/// `mcptest.yml` is the file a bare `mcptest run` resolves, so it must be a
/// schema-valid suite root that actually runs something (WOR-1352). Settings
/// the old scaffold carried as bare keys (`reporter`, `timeout_secs`) are not
/// suite-root keys in the v1 schema, so they live here as comments naming
/// their CLI flag equivalents; `run_options.parallel` is the one knob the
/// schema accepts in-file.
const PROJECT_CONFIG_HEADER: &str = r#"# yaml-language-server: $schema=https://mcptest.sh/schema/v1.json
#
# mcptest project suite scaffolded by `mcptest init`. A bare `mcptest run`
# from this directory finds this file and executes it, so the first run
# works without arguments.
#
# Settings that are not part of the suite schema stay CLI flags:
#   reporter style:   mcptest run --reporter pretty (or json, junit, ...)
#   per-test timeout: mcptest run --timeout 30

run_options:
  # auto uses one worker per logical CPU; false forces serial execution;
  # a positive integer pins the worker count.
  parallel: auto

"#;

/// Build the `mcptest.yml` body from the fork's example suite body.
///
/// Composes [`PROJECT_CONFIG_HEADER`] with the same suite the fork wrote to
/// `tests/example.yaml`, minus that body's own editor directive so the
/// `# yaml-language-server:` line stays unique and first in the file.
fn scaffold_project_config(example_body: &str) -> String {
    let mut out = String::from(PROJECT_CONFIG_HEADER);
    for line in example_body.lines() {
        if line.starts_with("# yaml-language-server:") {
            continue;
        }
        out.push_str(line);
        out.push('\n');
    }
    out
}

/// Optional `--with-jury` addendum appended to `tests/example.yaml`.
///
/// Emits a top-level `evals:` array (the shape `schemas/v1.json` accepts;
/// the old mapping form failed `mcptest validate`, WOR-1353) with one
/// rubric eval graded by a three-judge jury. Takes the fork's server key
/// so the eval references a server the scaffold actually declares. The
/// fixed `response:` keeps the eval deterministic; only running it needs
/// a provider key, validating it does not.
fn jury_appendix(server_key: &str) -> String {
    format!(
        r#"
# --- Jury eval (scaffolded by `mcptest init --with-jury`) ---
# `mcptest validate` accepts this block with no credentials. Running it is
# a separate command, `mcptest eval`, which grades the fixed `response:`
# below against the rubric with a jury of three independent judges. That
# grading calls a model, so it needs a provider API key in the environment
# (for example ANTHROPIC_API_KEY); without one the eval defers and is
# reported as passed with a note.
evals:
  - name: "greeting reads as short and friendly"
    server: {server_key}
    prompt: "Say hello to mcptest."
    # A fixed response keeps grading reproducible and server-free. Delete
    # it to grade a live tool-using agent run against the server instead.
    response: "Hello, mcptest!"
    rubric: "The response is a short, friendly greeting that names mcptest."
    # The eval passes when the jury's score reaches the threshold.
    threshold: 0.7
    judge:
      jury:
        # Three judges grade independently; the eval passes when at least
        # two thirds of them pass.
        size: 3
        consensus: 0.66
"#
    )
}

/// Entry point for `mcptest init`.
///
/// Returns the desired process exit code. `0` on success. `2` when a target
/// file already exists and `--force` was not supplied. When `url_target`
/// is `Some`, scaffolds a URL-keyed `server:` block; otherwise emits the
/// stdio template (which also writes the `mcptest mock` catalog so the
/// first run is offline and green).
pub fn run_init(
    target_dir: &Path,
    with_jury: bool,
    force: bool,
    quiet: bool,
    url_target: Option<UrlTarget>,
    discovered: Option<DiscoveredTarget>,
) -> Result<i32> {
    let tests_dir = target_dir.join("tests");
    let example_path = tests_dir.join("example.yaml");
    let config_path = target_dir.join("mcptest.yml");
    let manifest_path = tests_dir.join("example-server.yml");

    // The mock catalog only backs the default stdio template; the URL and
    // discovered forks target a server the user already has.
    let writes_manifest = url_target.is_none() && discovered.is_none();
    let mut candidates: Vec<&Path> = vec![&example_path, &config_path];
    if writes_manifest {
        candidates.push(&manifest_path);
    }
    let conflicts = existing_files(&candidates);
    if !conflicts.is_empty() && !force {
        for path in &conflicts {
            eprintln!("mcptest init: refusing to overwrite {}", path.display());
        }
        eprintln!("mcptest init: pass --force to overwrite existing files.");
        return Ok(2);
    }

    fs::create_dir_all(&tests_dir)
        .with_context(|| format!("failed to create {}", tests_dir.display()))?;

    let mut example_body = match (&discovered, &url_target) {
        (Some(target), _) => scaffold_discovered_example(target),
        (None, Some(target)) => scaffold_url_example(target),
        (None, None) => format!("{EXAMPLE_HEADER}{EXAMPLE_SUITE_BODY}"),
    };
    // mcptest.yml mirrors the example suite, composed before the jury
    // appendix so a `--with-jury` scaffold cannot break the bare run.
    let config_body = scaffold_project_config(&example_body);
    if with_jury {
        // The eval must reference a server key the fork actually declares:
        // the discovered fork names the server after the client-config key,
        // the stdio and URL forks both call it `example`.
        let server_key = discovered
            .as_ref()
            .map(|t| t.name.as_str())
            .unwrap_or("example");
        example_body.push_str(&jury_appendix(server_key));
    }
    fs::write(&example_path, example_body)
        .with_context(|| format!("failed to write {}", example_path.display()))?;
    if writes_manifest {
        fs::write(&manifest_path, EXAMPLE_MOCK_MANIFEST)
            .with_context(|| format!("failed to write {}", manifest_path.display()))?;
    }
    fs::write(&config_path, config_body)
        .with_context(|| format!("failed to write {}", config_path.display()))?;

    if !quiet {
        println!("Created {}", example_path.display());
        if writes_manifest {
            println!("Created {}", manifest_path.display());
        }
        println!("Created {}", config_path.display());
        println!("Next: mcptest run");
    }
    Ok(0)
}

/// Build the YAML body for a URL-target example.
///
/// Emits a `servers.example` block with `url:`, an `auth:` mapping shaped
/// to the chosen recipe, and an optional `wait_for_ready:` field. The
/// trailing test exercises `tools/list` so the smoke run does not depend
/// on a server-specific tool name.
pub fn scaffold_url_example(target: &UrlTarget) -> String {
    let mut out = String::new();
    out.push_str("# yaml-language-server: $schema=https://mcptest.sh/schema/v1.json\n");
    out.push_str("#\n");
    out.push_str("# URL-target mcptest suite scaffolded by `mcptest init --url`.\n");
    out.push_str("# See https://mcptest.sh/docs/url-targets for the full recipe list.\n\n");
    out.push_str("servers:\n");
    out.push_str("  example:\n");
    out.push_str(&format!("    url: \"{}\"\n", target.url));
    render_auth_block(&mut out, &target.auth);
    if let Some(budget) = target.wait_for_ready {
        out.push_str(&format!("    wait_for_ready: {}s\n", budget.as_secs()));
    }
    out.push('\n');
    out.push_str("tools:\n");
    out.push_str("  - name: \"server advertises a tools list without error\"\n");
    out.push_str("    server: example\n");
    out.push_str("    tool: \"__mcptest_probe__\"\n");
    out.push_str("    args: {}\n");
    out.push_str("    expect:\n");
    out.push_str("      - target: \"error\"\n");
    out.push_str("        matcher:\n");
    out.push_str("          exact: null\n");
    out.push_str("        message: \"probe call should not return a transport error\"\n");
    out
}

/// Append the `auth:` mapping under the `servers.example:` block.
fn render_auth_block(out: &mut String, auth: &InitAuth) {
    match auth {
        InitAuth::None => {
            out.push_str("    # No authentication. The server accepts open requests.\n");
        }
        InitAuth::Bearer { env_var } => {
            out.push_str("    auth:\n");
            out.push_str(&format!("      bearer_token_env: {env_var}\n"));
        }
        InitAuth::OAuth2 => {
            out.push_str("    auth:\n");
            out.push_str("      oauth:\n");
            out.push_str("        # Run `mcptest login` once to cache an access token.\n");
            out.push_str("        # The runner refreshes the token automatically per .\n");
            out.push_str("        flow: authorization_code_pkce\n");
        }
        InitAuth::CustomHeader { name, env_var } => {
            out.push_str("    headers:\n");
            match env_var {
                Some(var) => {
                    out.push_str(&format!("      {name}-Env: {var}\n"));
                }
                None => {
                    out.push_str(&format!("      {name}: \"REPLACE_ME\"\n"));
                }
            }
        }
    }
}

/// Return the subset of `candidates` that already exist on disk.
fn existing_files(candidates: &[&Path]) -> Vec<PathBuf> {
    candidates
        .iter()
        .filter(|p| p.exists())
        .map(|p| p.to_path_buf())
        .collect()
}

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

    #[test]
    fn creates_files_in_empty_directory() {
        let dir = tempdir().unwrap();
        let code = run_init(dir.path(), false, false, true, None, None).unwrap();
        assert_eq!(code, 0);
        assert!(dir.path().join("tests/example.yaml").exists());
        assert!(dir.path().join("tests/example-server.yml").exists());
        assert!(dir.path().join("mcptest.yml").exists());
    }

    #[test]
    fn scaffolded_files_pass_suite_schema_validation() {
        let dir = tempdir().unwrap();
        run_init(dir.path(), false, false, true, None, None).unwrap();
        for file in ["mcptest.yml", "tests/example.yaml"] {
            let result = mcptest_config::loader::load_and_validate(
                &dir.path().join(file),
                mcptest_config::loader::LoadOptions::default(),
            );
            assert!(result.is_ok(), "{file} should validate: {result:?}");
        }
    }

    #[test]
    fn project_config_mirrors_the_example_suite() {
        let dir = tempdir().unwrap();
        run_init(dir.path(), false, false, true, None, None).unwrap();
        let body = fs::read_to_string(dir.path().join("mcptest.yml")).unwrap();
        assert!(body.contains("servers:"), "mcptest.yml declares the server");
        assert!(body.contains("say_hello"), "mcptest.yml carries the test");
        assert!(body.contains("parallel: auto"), "run_options survive");
    }

    #[test]
    fn jury_appendix_stays_out_of_the_project_config() {
        let dir = tempdir().unwrap();
        run_init(dir.path(), true, false, true, None, None).unwrap();
        let config = fs::read_to_string(dir.path().join("mcptest.yml")).unwrap();
        assert!(
            !config.contains("evals:"),
            "the jury evals block must not ride into the bare-run suite"
        );
    }

    #[test]
    fn url_fork_skips_the_mock_manifest_and_mirrors_the_url_suite() {
        let dir = tempdir().unwrap();
        let target = UrlTarget {
            url: "https://mcp.example.com/v1".into(),
            auth: InitAuth::None,
            wait_for_ready: None,
        };
        run_init(dir.path(), false, false, true, Some(target), None).unwrap();
        assert!(!dir.path().join("tests/example-server.yml").exists());
        let config = fs::read_to_string(dir.path().join("mcptest.yml")).unwrap();
        assert!(config.contains("url: \"https://mcp.example.com/v1\""));
    }

    #[test]
    fn refuses_to_overwrite_existing_file() {
        let dir = tempdir().unwrap();
        fs::create_dir_all(dir.path().join("tests")).unwrap();
        fs::write(dir.path().join("tests/example.yaml"), "old").unwrap();
        let code = run_init(dir.path(), false, false, true, None, None).unwrap();
        assert_eq!(code, 2);
        let body = fs::read_to_string(dir.path().join("tests/example.yaml")).unwrap();
        assert_eq!(body, "old", "existing file should be untouched");
    }

    #[test]
    fn force_flag_overwrites_existing_file() {
        let dir = tempdir().unwrap();
        fs::create_dir_all(dir.path().join("tests")).unwrap();
        fs::write(dir.path().join("tests/example.yaml"), "old").unwrap();
        let code = run_init(dir.path(), false, true, true, None, None).unwrap();
        assert_eq!(code, 0);
        let body = fs::read_to_string(dir.path().join("tests/example.yaml")).unwrap();
        assert!(body.contains("say_hello"), "scaffold replaced the old file");
    }

    #[test]
    fn with_jury_appends_an_evals_array_with_a_jury_judge() {
        let dir = tempdir().unwrap();
        let code = run_init(dir.path(), true, false, true, None, None).unwrap();
        assert_eq!(code, 0);
        let body = fs::read_to_string(dir.path().join("tests/example.yaml")).unwrap();
        assert!(body.contains("evals:"));
        assert!(body.contains("jury:"));
        assert!(body.contains("server: example"));
    }

    #[test]
    fn with_jury_scaffold_passes_suite_schema_validation() {
        let dir = tempdir().unwrap();
        run_init(dir.path(), true, false, true, None, None).unwrap();
        let result = mcptest_config::loader::load_and_validate(
            &dir.path().join("tests/example.yaml"),
            mcptest_config::loader::LoadOptions::default(),
        );
        assert!(result.is_ok(), "jury scaffold should validate: {result:?}");
    }

    #[test]
    fn with_jury_eval_references_the_discovered_server_key() {
        let dir = tempdir().unwrap();
        let target = DiscoveredTarget {
            name: "github".into(),
            command: "npx".into(),
            args: vec!["-y".into(), "@modelcontextprotocol/server-github".into()],
            env_names: vec![],
        };
        run_init(dir.path(), true, false, true, None, Some(target)).unwrap();
        let body = fs::read_to_string(dir.path().join("tests/example.yaml")).unwrap();
        assert!(
            body.contains("server: github"),
            "the jury eval must point at the discovered server, got:\n{body}"
        );
    }

    #[test]
    fn url_target_emits_url_keyed_server_block() {
        let dir = tempdir().unwrap();
        let target = UrlTarget {
            url: "https://mcp.example.com/v1".into(),
            auth: InitAuth::None,
            wait_for_ready: None,
        };
        let code = run_init(dir.path(), false, false, true, Some(target), None).unwrap();
        assert_eq!(code, 0);
        let body = fs::read_to_string(dir.path().join("tests/example.yaml")).unwrap();
        assert!(body.contains("url: \"https://mcp.example.com/v1\""));
        assert!(!body.contains("command:"));
    }

    #[test]
    fn discovered_target_scaffolds_a_stdio_block_without_secret_values() {
        let dir = tempdir().unwrap();
        let target = DiscoveredTarget {
            name: "github".into(),
            command: "npx".into(),
            args: vec!["-y".into(), "@modelcontextprotocol/server-github".into()],
            env_names: vec!["GITHUB_TOKEN".into()],
        };
        let code = run_init(dir.path(), false, false, true, None, Some(target)).unwrap();
        assert_eq!(code, 0);
        let body = fs::read_to_string(dir.path().join("tests/example.yaml")).unwrap();
        // The discovered command and server key are scaffolded.
        assert!(body.contains("server-github"));
        assert!(body.contains("github:"));
        // The env-var name is surfaced as guidance, but no value is inlined.
        assert!(body.contains("GITHUB_TOKEN"));
        assert!(!body.contains("url:"));
    }

    #[test]
    fn bearer_auth_renders_bearer_token_env_block() {
        let target = UrlTarget {
            url: "https://mcp.example.com/v1".into(),
            auth: InitAuth::Bearer {
                env_var: "MCPTEST_API_TOKEN".into(),
            },
            wait_for_ready: None,
        };
        let yaml = scaffold_url_example(&target);
        assert!(yaml.contains("bearer_token_env: MCPTEST_API_TOKEN"));
    }

    #[test]
    fn oauth2_renders_oauth_block() {
        let target = UrlTarget {
            url: "https://mcp.example.com/v1".into(),
            auth: InitAuth::OAuth2,
            wait_for_ready: None,
        };
        let yaml = scaffold_url_example(&target);
        assert!(yaml.contains("oauth:"));
        assert!(yaml.contains("authorization_code_pkce"));
    }

    #[test]
    fn custom_header_with_env_var_renders_env_suffix() {
        let target = UrlTarget {
            url: "https://mcp.example.com/v1".into(),
            auth: InitAuth::CustomHeader {
                name: "CF-Access-Client-Id".into(),
                env_var: Some("CF_ACCESS_CLIENT_ID".into()),
            },
            wait_for_ready: None,
        };
        let yaml = scaffold_url_example(&target);
        assert!(yaml.contains("CF-Access-Client-Id-Env: CF_ACCESS_CLIENT_ID"));
    }

    #[test]
    fn custom_header_without_env_var_renders_literal_placeholder() {
        let target = UrlTarget {
            url: "https://mcp.example.com/v1".into(),
            auth: InitAuth::CustomHeader {
                name: "X-API-Key".into(),
                env_var: None,
            },
            wait_for_ready: None,
        };
        let yaml = scaffold_url_example(&target);
        assert!(yaml.contains("X-API-Key: \"REPLACE_ME\""));
    }

    #[test]
    fn wait_for_ready_emits_duration_field() {
        let target = UrlTarget {
            url: "https://mcp.example.com/v1".into(),
            auth: InitAuth::None,
            wait_for_ready: Some(Duration::from_secs(45)),
        };
        let yaml = scaffold_url_example(&target);
        assert!(yaml.contains("wait_for_ready: 45s"));
    }
}
