//! End-to-end cancellation-timing race (WOR-1364).
//!
//! Validates the Client fire-and-observe primitive (WOR-1407,
//! `send_request`/`response_within`) against the real `mcptest mock` subprocess
//! over stdio, exercising the cancel-after-completion race the cancellation pack
//! targets:
//!
//! - a `reply-after-cancel` mock replies even after the request was cancelled
//!   (the non-conformant race) -> `response_within` observes `Some`,
//! - a `slow` mock suppresses the cancelled response (conforming) ->
//!   `response_within` observes `None`.
//!
//! The mock fault is deterministic (it reads the cancel before the delayed
//! response is due), so the timing is reproducible; the generous observe windows
//! absorb subprocess scheduling jitter without making the assertion timing-bound.

use std::collections::BTreeMap;
use std::time::Duration;

use mcptest_core::connector::connect_server;
use mcptest_core::network::ProxyConfig;
use mcptest_core::suite::ServerSpec;
use serde_json::{json, Value};
use tempfile::TempDir;

/// Write a one-tool mock manifest and return (tempdir, path). The tempdir is
/// returned so the caller keeps it alive for the subprocess's lifetime.
fn write_manifest() -> (TempDir, String) {
    let dir = tempfile::tempdir().unwrap();
    let body = r#"mock_server:
  name: race-fixture
  tools:
    - name: echo
      description: "Echo back."
      input_schema:
        type: object
        required: [msg]
        properties:
          msg: { type: string }
      response:
        content:
          - type: text
            text: "echo: ${args.msg}"
"#;
    let path = dir.path().join("tools.yml");
    std::fs::write(&path, body).unwrap();
    let path_str = path.to_str().unwrap().to_string();
    (dir, path_str)
}

/// Spawn `mcptest mock --tools-from <manifest> --fault <fault>`, send a
/// `tools/call`, cancel it in flight, and observe the reply within `observe`.
async fn observe_cancel_race(fault: &str, observe: Duration) -> Option<Value> {
    let (_dir, manifest) = write_manifest();
    let spec = ServerSpec::Stdio {
        command: env!("CARGO_BIN_EXE_mcptest").to_string(),
        args: vec![
            "mock".to_string(),
            "--tools-from".to_string(),
            manifest,
            "--fault".to_string(),
            fault.to_string(),
        ],
        env: BTreeMap::new(),
    };
    let server = connect_server(
        "race",
        &spec,
        "mcptest-cancel-race-test",
        "0.0.0",
        &ProxyConfig::default(),
        &[],
    )
    .await
    .expect("mock connects");

    let pending = server
        .client
        .send_request(
            "tools/call",
            json!({ "name": "echo", "arguments": { "msg": "hi" } }),
        )
        .await
        .expect("tools/call sends");
    // Cancel the in-flight request, then observe whether the server still replies.
    server
        .client
        .notify(
            "notifications/cancelled",
            json!({ "requestId": pending.id() }),
        )
        .await
        .expect("cancel notification sends");
    let observed = pending
        .response_within(observe)
        .await
        .expect("observe does not error");
    let _ = server.shutdown.shutdown().await;
    observed
}

#[tokio::test]
async fn reply_after_cancel_mock_is_observed_as_the_race() {
    // The non-conformant fixture fires the response despite the cancellation,
    // so the client observes a reply within the window.
    let observed = observe_cancel_race("reply-after-cancel:50", Duration::from_secs(5)).await;
    assert!(
        observed.is_some(),
        "a reply-after-cancel server replies after the cancel: {observed:?}"
    );
}

#[tokio::test]
async fn slow_mock_suppresses_the_cancelled_response() {
    // The conforming fixture suppresses the cancelled response, so the client
    // observes nothing within the window.
    let observed = observe_cancel_race("slow:50", Duration::from_millis(750)).await;
    assert!(
        observed.is_none(),
        "a conforming server sends nothing after the cancel: {observed:?}"
    );
}
