//! Request/response transform step end to end.
//!
//! Drives `mcptest run` against a deterministic stdio fixture server with a
//! tool test that declares a `transform:` block. The request transform
//! rewrites the outbound `tools/call` params before they are sent; the
//! response transform rewrites the `{"result": ...}` envelope before
//! assertions run. The fixture server reflects the arguments it received
//! back into the result text, so an assertion on the reflected value proves
//! the request transform actually reached the wire, and an assertion on the
//! transformed envelope proves the response transform ran before matching.
//!
//! POSIX shell only (the transforms and the fixture server use `/bin/sh`),
//! so the whole test is Unix-gated like the other spawn-heavy integration
//! tests.

#![cfg(unix)]

use std::os::unix::fs::PermissionsExt;
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
/// received `q` argument into the result text (`got <q>`), and return the
/// JSON-array server command to splice into a suite's `command:`. Reflecting
/// `q` lets the test observe whether the request transform rewrote the
/// outbound params. 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()
    )
}

fn write_executable(dir: &Path, name: &str, body: &str) -> std::path::PathBuf {
    let path = dir.join(name);
    std::fs::write(&path, body).expect("write script");
    let mut perms = std::fs::metadata(&path).expect("meta").permissions();
    perms.set_mode(0o755);
    std::fs::set_permissions(&path, perms).expect("chmod");
    path
}

#[test]
fn request_transform_rewrites_outbound_args_before_send() {
    let dir = tempfile::tempdir().unwrap();
    let cmd = echo_server_command(dir.path());
    // The request transform replaces the whole params with a fixed object
    // whose arguments carry `q: transformed`. The server reflects those
    // arguments into the result text, so the assertion sees the rewrite.
    let req_transform = write_executable(
        dir.path(),
        "req.sh",
        "#!/bin/sh\ncat >/dev/null\necho '{\"name\":\"echo\",\"arguments\":{\"q\":\"transformed\"}}'\n",
    );
    let yaml = format!(
        r#"servers:
  fixture:
    command: {cmd}
tools:
  - name: request transform rewrites args
    server: fixture
    tool: echo
    args:
      q: original
    transform:
      request: {req}
    expect:
      - target: result.content[0].text
        matcher:
          regex: "^got transformed$"
"#,
        req = req_transform.display(),
    );
    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"));
}

#[test]
fn response_transform_normalizes_envelope_before_matching() {
    let dir = tempfile::tempdir().unwrap();
    let cmd = echo_server_command(dir.path());
    // The response transform discards the real envelope and emits a
    // normalized one whose result carries `status: normalized`. The
    // assertion passes only because the transform ran before matching.
    let resp_transform = write_executable(
        dir.path(),
        "resp.sh",
        "#!/bin/sh\ncat >/dev/null\necho '{\"result\":{\"status\":\"normalized\"}}'\n",
    );
    let yaml = format!(
        r#"servers:
  fixture:
    command: {cmd}
tools:
  - name: response transform normalizes envelope
    server: fixture
    tool: echo
    args:
      q: original
    transform:
      response: {resp}
    expect:
      - target: result.status
        matcher:
          exact: normalized
"#,
        resp = resp_transform.display(),
    );
    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"));
}

#[test]
fn transform_failure_fails_the_test_with_a_clear_message() {
    let dir = tempfile::tempdir().unwrap();
    let cmd = echo_server_command(dir.path());
    // A transform that exits non-zero must fail the test and name the
    // phase and command in the failure message.
    let failing = write_executable(
        dir.path(),
        "boom.sh",
        "#!/bin/sh\ncat >/dev/null\necho boom >&2\nexit 5\n",
    );
    let yaml = format!(
        r#"servers:
  fixture:
    command: {cmd}
tools:
  - name: failing response transform
    server: fixture
    tool: echo
    args:
      q: original
    transform:
      response: {boom}
    expect:
      - target: result.status
        matcher:
          exact: never
"#,
        boom = failing.display(),
    );
    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(1)
        // Default pretty prints the per-test failure detail inline on stdout.
        .stdout(
            predicate::str::contains("response transform").and(predicate::str::contains("exited")),
        );
}
