//! `${var}` interpolation on the live `run` path.
//!
//! Drives `mcptest run` against a deterministic stdio fixture server with a
//! suite that declares a `variables:` block and uses `${name}` inside a
//! tool's `args:`. The fixture server reflects the `q` argument it received
//! back into the result text, so an assertion on the reflected value proves
//! the suite's variable reached the wire resolved (not the literal
//! `${name}`). This locks in the regression the run path used to have where
//! `${...}` in args and commands was passed through verbatim.
//!
//! POSIX shell only (the fixture server uses `/bin/sh`), so the whole test
//! is Unix-gated like the other spawn-heavy integration tests.

#![cfg(unix)]

use std::path::Path;

use assert_cmd::Command;
use predicates::prelude::*;

fn mcptest() -> Command {
    Command::cargo_bin("mcptest").expect("binary builds")
}

/// Write a `mcptest mock` manifest exposing an `echo` tool that reflects the
/// `q` argument into the result text (`got <q>`), and return the JSON-array
/// server command to splice into a suite's `command:`. The native mock
/// answers the handshake instantly, unlike a fork-per-line shell fixture.
fn echo_server_command(dir: &Path) -> String {
    let manifest = dir.join("echo.yaml");
    std::fs::write(
        &manifest,
        r#"mock_server:
  name: echo-fixture
  tools:
    - name: echo
      description: "Echo the arguments back"
      input_schema:
        type: object
        properties:
          q: { type: string }
      response:
        content:
          - type: text
            text: "got ${args.q}"
"#,
    )
    .expect("write mock manifest");
    let bin = env!("CARGO_BIN_EXE_mcptest");
    format!(
        r#"['{bin}', 'mock', '--tools-from', '{}']"#,
        manifest.display()
    )
}

#[test]
fn variable_interpolates_into_tool_args_at_run_time() {
    let dir = tempfile::tempdir().unwrap();
    let cmd = echo_server_command(dir.path());
    let yaml = format!(
        r#"variables:
  who:
    value: world
servers:
  fixture:
    command: {cmd}
tools:
  - name: variable reaches the wire
    server: fixture
    tool: echo
    args:
      q: "${{who}}"
    expect:
      - target: result.content[0].text
        matcher:
          exact: "got world"
"#,
    );
    let config = dir.path().join("mcptest.yaml");
    std::fs::write(&config, yaml).expect("write suite");

    mcptest()
        .current_dir(dir.path())
        .args(["run", "--config"])
        .arg(&config)
        .assert()
        .code(0)
        .stdout(predicate::str::contains("1 passed"));
}

/// A value supplied only through `--env-file` reaches the run. The run path
/// reads `${...}` references (and the agent provider's credential lookup)
/// straight from `std::env`, so an `--env-file` value must be exported into
/// the process environment before the run resolves anything. This guards the
/// regression where env-file values were parsed into the variable map but
/// never applied to `std::env`, so an API key in a dotenv file silently fell
/// back to the stub provider. Here the value flows into a tool argument and
/// the mock reflects it back, proving it reached the wire resolved.
#[test]
fn env_file_value_reaches_the_run_via_process_env() {
    let dir = tempfile::tempdir().unwrap();
    let cmd = echo_server_command(dir.path());
    // No `variables:` block and no `--var`: the only source for `${who}` is
    // the env file. The env var name is unique so a stray ambient value cannot
    // mask a regression.
    let yaml = format!(
        r#"servers:
  fixture:
    command: {cmd}
tools:
  - name: env-file value reaches the wire
    server: fixture
    tool: echo
    args:
      q: "${{MCPTEST_ENVFILE_INTERP_VALUE}}"
    expect:
      - target: result.content[0].text
        matcher:
          exact: "got from-env-file"
"#,
    );
    let config = dir.path().join("mcptest.yaml");
    std::fs::write(&config, yaml).expect("write suite");

    let env_file = dir.path().join("creds.env");
    std::fs::write(&env_file, "MCPTEST_ENVFILE_INTERP_VALUE=from-env-file\n").expect("write env");

    mcptest()
        .current_dir(dir.path())
        .args(["run", "--config"])
        .arg(&config)
        .arg("--env-file")
        .arg(&env_file)
        // Strip any ambient value so the env file is the only source.
        .env_remove("MCPTEST_ENVFILE_INTERP_VALUE")
        .assert()
        .code(0)
        .stdout(predicate::str::contains("1 passed"));
}

#[test]
fn from_env_variable_default_interpolates_into_tool_args() {
    let dir = tempfile::tempdir().unwrap();
    let cmd = echo_server_command(dir.path());
    // `from_env` names an env var that is not set, so the declared `default`
    // is used. Proves the from_env/default branch of the run-path context.
    let yaml = format!(
        r#"variables:
  who:
    from_env: MCPTEST_VAR_INTERP_DEFINITELY_UNSET
    default: fallback
servers:
  fixture:
    command: {cmd}
tools:
  - name: from_env default reaches the wire
    server: fixture
    tool: echo
    args:
      q: "${{who}}"
    expect:
      - target: result.content[0].text
        matcher:
          exact: "got fallback"
"#,
    );
    let config = dir.path().join("mcptest.yaml");
    std::fs::write(&config, yaml).expect("write suite");

    mcptest()
        .current_dir(dir.path())
        .args(["run", "--config"])
        .arg(&config)
        .assert()
        .code(0)
        .stdout(predicate::str::contains("1 passed"));
}
