//! Integration tests for `--wait-for-ready`.
//!
//! The polling loop and backoff are unit-tested in
//! `mcptest-core::readiness::wait`; the per-target probe and orchestration in
//! `cli::handlers::readiness`. These tests pin the user-facing contract on the
//! run and doctor commands: a URL server that never comes up aborts with exit
//! code 3, and a stdio-only suite has nothing to wait for so the flag is a
//! no-op. Flag parsing (bare flag default, duration units) lives in
//! `cli::tests`.

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

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

/// A loopback port that stays connection-refused for the whole test.
///
/// The old bind-then-drop trick returned an ephemeral port the OS could
/// immediately hand to a concurrent test's mock server, so the probe
/// sometimes connected and the doctor proceeded to a normal failure
/// (exit 1) instead of the readiness timeout (exit 3); this flaked on the
/// macOS CI runner. Port 1 is in the privileged range, so an unprivileged
/// CI process cannot bind it between probe attempts, and nothing on a CI
/// runner listens there (asserted below so an exotic host fails loudly
/// rather than flaking).
fn dead_port() -> u16 {
    let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 1));
    let probe = std::net::TcpStream::connect_timeout(&addr, std::time::Duration::from_millis(200));
    assert!(
        probe.is_err(),
        "something is listening on 127.0.0.1:1; dead_port needs a closed privileged port"
    );
    1
}

#[test]
fn run_wait_for_ready_times_out_with_exit_3_when_server_never_comes_up() {
    let port = dead_port();
    let dir = tempfile::tempdir().unwrap();
    let cfg = dir.path().join("mcptest.yaml");
    std::fs::write(
        &cfg,
        format!(
            r#"servers:
  api:
    url: "http://127.0.0.1:{port}/mcp"
tools:
  - name: never reached
    server: api
    tool: ping
    expect:
      - target: result.content[0].text
        matcher: {{ exact: ok }}
"#
        ),
    )
    .unwrap();

    mcptest()
        .args([
            "run",
            "--config",
            cfg.to_str().unwrap(),
            "--wait-for-ready=2s",
        ])
        .assert()
        .code(3)
        .stderr(predicate::str::contains("not ready within the budget"));
}

#[test]
fn run_wait_for_ready_is_a_no_op_for_a_stdio_only_suite() {
    // A stdio server is spawned by mcptest, so there is nothing to wait for.
    // The mcptest mock is a deterministic, key-free stdio server.
    let bin = assert_cmd::cargo::cargo_bin("mcptest");
    let dir = tempfile::tempdir().unwrap();
    let manifest = dir.path().join("tools.yaml");
    std::fs::write(
        &manifest,
        r#"mock_server:
  name: echo
  tools:
    - name: echo
      description: "Echo a message."
      input_schema:
        type: object
        required: [msg]
        properties:
          msg: { type: string }
      response:
        content:
          - type: text
            text: "echo: ${args.msg}"
"#,
    )
    .unwrap();
    let cfg = dir.path().join("mcptest.yaml");
    std::fs::write(
        &cfg,
        format!(
            r#"servers:
  echo:
    command: ['{bin}', 'mock', '--tools-from', '{manifest}']
tools:
  - name: echo works
    server: echo
    tool: echo
    args: {{ msg: hello }}
    expect:
      - target: result.content[0].text
        matcher: {{ icontains: "echo: hello" }}
"#,
            bin = bin.display(),
            manifest = manifest.display(),
        ),
    )
    .unwrap();

    mcptest()
        .args([
            "run",
            "--config",
            cfg.to_str().unwrap(),
            "--wait-for-ready=5s",
        ])
        .assert()
        .success()
        .stdout(predicate::str::contains("no URL servers to probe"))
        .stdout(predicate::str::contains("1 passed"));
}

#[test]
fn doctor_wait_for_ready_times_out_with_exit_3_against_a_dead_url() {
    let port = dead_port();
    mcptest()
        .args([
            "doctor",
            "--url",
            &format!("http://127.0.0.1:{port}/mcp"),
            "--wait-for-ready=2s",
        ])
        .assert()
        .code(3)
        .stderr(predicate::str::contains("not ready within the budget"));
}

#[test]
fn doctor_wait_for_ready_without_url_is_skipped() {
    mcptest()
        .args(["doctor", "--wait-for-ready=2s"])
        .assert()
        .success()
        .stdout(predicate::str::contains("no --url to probe; skipping"));
}
