//! Integration tests for `mcptest conformance run --client` (WOR-1365).
//!
//! The client lane stands mcptest up as an adversarial server and points a
//! client-under-test at it. These stub the client with a shell script that
//! speaks MCP over its stdio, so no real SDK is needed. Unix-only because the
//! stub is a shell script.

#![cfg(unix)]

use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};

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

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

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

/// A conformant client: initializes, announces initialized, answers ping, and
/// re-lists tools after a mid-session `notifications/tools/list_changed` so it
/// passes the cross-server-shadowing probe (WOR-1471).
const GOOD_CLIENT: &str = r##"#!/bin/sh
printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","clientInfo":{"name":"good","version":"0.0.1"},"capabilities":{}}}\n'
IFS= read -r _init_response
printf '{"jsonrpc":"2.0","method":"notifications/initialized"}\n'
while IFS= read -r line; do
  case "$line" in
    *'"id":'*) tmp=${line#*'"id":'}; id=${tmp%%[!0-9-]*} ;;
    *) id= ;;
  esac
  case "$line" in
    *'"method":"ping"'*) [ -n "$id" ] && printf '{"jsonrpc":"2.0","id":%s,"result":{}}\n' "$id" ;;
    *'"method":"notifications/tools/list_changed"'*) printf '{"jsonrpc":"2.0","id":7,"method":"tools/list"}\n' ;;
  esac
done
"##;

/// A buggy client: never sends notifications/initialized, never answers ping.
const BUGGY_CLIENT: &str = r##"#!/bin/sh
printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","clientInfo":{"name":"buggy","version":"0.0.1"},"capabilities":{}}}\n'
IFS= read -r _init_response
while IFS= read -r _line; do
  :
done
"##;

#[test]
fn a_conformant_client_scores_tier_one_through_the_cli() {
    let tmp = tempfile::tempdir().unwrap();
    let client = write_client(tmp.path(), "good.sh", GOOD_CLIENT);
    mcptest()
        .arg("conformance")
        .arg("run")
        .arg("--client")
        .arg(client.to_string_lossy().as_ref())
        .arg("--format")
        .arg("json")
        .assert()
        .success()
        .stdout(predicate::str::contains("\"scope\": \"client-probe\""))
        .stdout(predicate::str::contains("\"tier\": \"tier1\""))
        .stdout(predicate::str::contains("client-responds-to-ping"));
}

#[test]
fn a_buggy_client_fails_the_lane_but_the_run_still_exits_zero() {
    let tmp = tempfile::tempdir().unwrap();
    let client = write_client(tmp.path(), "buggy.sh", BUGGY_CLIENT);
    // A nonconformant client is reported in the badge, not the exit code, so the
    // run still exits 0 (parse the JSON to gate CI on the tier).
    mcptest()
        .arg("conformance")
        .arg("run")
        .arg("--client")
        .arg(client.to_string_lossy().as_ref())
        .arg("--format")
        .arg("json")
        .assert()
        .success()
        .stdout(predicate::str::contains("\"tier\": \"fail\""))
        .stdout(predicate::str::contains("client-initialized-notification"));
}

#[test]
fn client_and_server_are_mutually_exclusive() {
    mcptest()
        .arg("conformance")
        .arg("run")
        .arg("--client")
        .arg("true")
        .arg("--server")
        .arg("http://127.0.0.1:9/mcp")
        .assert()
        .failure()
        .stderr(predicate::str::contains("cannot be used with"));
}
