//! Mock manifest data model and loaders.
//!
//! The declarative tool / resource / prompt catalog the operator-facing mock
//! serves, loaded from a YAML `mock_server.tools[]` manifest or a JSON
//! `tools/list` snapshot. Both formats converge on [`MockManifest`] so the
//! dispatcher does not care which produced it. Split out of the serve loop so
//! `mock/mod.rs` stays under the module-size guard.

use std::fs;
use std::path::Path;

use anyhow::{anyhow, Context, Result};
use serde::Deserialize;
use serde_json::Value;

/// One tool entry in a mock manifest.
///
/// Mirrors the shape `tools/list` returns on the wire so a captured baseline
/// JSON can deserialize without re-mapping. The optional `response` field is
/// the mock-specific extension; baselines without it fall back to a generic
/// `{ "content": [{ "type": "text", "text": "mock <name>" }] }` payload.
#[derive(Debug, Clone, Default, Deserialize)]
pub struct MockTool {
    /// Wire name of the tool.
    pub name: String,
    /// Optional human-readable description echoed back from `tools/list`.
    #[serde(default)]
    pub description: Option<String>,
    /// Optional JSON Schema describing valid call arguments.
    #[serde(default, rename = "input_schema", alias = "inputSchema")]
    pub input_schema: Option<Value>,
    /// Optional JSON Schema describing the tool's structured output.
    /// Advertised verbatim in `tools/list` (WOR-1276) so output-schema
    /// conformance tests can run against the mock.
    #[serde(default, rename = "output_schema", alias = "outputSchema")]
    pub output_schema: Option<Value>,
    /// Optional MCP tool annotations (`readOnlyHint`, `destructiveHint`,
    /// `idempotentHint`, `openWorldHint`). Advertised verbatim in
    /// `tools/list` (WOR-1276) so the execution safety policy can be
    /// exercised against the mock instead of falling back to the name
    /// heuristic.
    #[serde(default)]
    pub annotations: Option<Value>,
    /// Canned response. When unset, the mock synthesizes a generic text reply
    /// so the catalog still answers `tools/call` cleanly.
    #[serde(default)]
    pub response: Option<Value>,
    /// When set, calling this tool makes the mock first send a server-initiated
    /// `sampling/createMessage` request to the client with these params, then
    /// hold the tool response until the client replies (WOR-1407). Lets the
    /// client/host conformance lane assert how a client services sampling.
    #[serde(default)]
    pub sampling: Option<Value>,
    /// When set, calling this tool first sends a server-initiated
    /// `elicitation/create` request with these params and defers the tool
    /// response until the client replies (WOR-1407).
    #[serde(default)]
    pub elicitation: Option<Value>,
    /// When true, calling this tool first sends a server-initiated `roots/list`
    /// request (no params) and defers the tool response until the client
    /// replies (WOR-1407).
    #[serde(default, rename = "roots_list", alias = "rootsList")]
    pub roots_list: bool,
    /// When set, calling this tool returns a Tasks-extension handle
    /// (`CreateTaskResult`) instead of the result, and the client drives
    /// it with `tasks/get` polling and `tasks/cancel` (SEP-2663). The
    /// tool's normal canned response becomes the task's terminal
    /// `completed` result.
    #[serde(default)]
    pub task: Option<TaskSpec>,
    /// When set, the first `tools/call` returns an `InputRequiredResult`
    /// (`inputRequests` + `requestState`); a retry that echoes the
    /// `requestState` and supplies `inputResponses` gets the tool's
    /// normal response (SEP-2322 multi-round-trip).
    #[serde(default, rename = "input_required", alias = "inputRequired")]
    pub input_required: Option<InputRequiredSpec>,
}

/// Tasks-extension behavior for a mock tool (SEP-2663).
///
/// `poll_count` is how many `tasks/get` polls return `working` before the
/// task reaches `completed`; `0` completes on the first poll.
#[derive(Debug, Clone, Default, Deserialize)]
pub struct TaskSpec {
    /// Number of `working` polls before the task completes.
    #[serde(default, rename = "poll_count", alias = "pollCount")]
    pub poll_count: u32,
    /// Suggested poll interval advertised in the handle, milliseconds.
    /// Omitted from the wire when unset.
    #[serde(default, rename = "poll_interval_ms", alias = "pollIntervalMs")]
    pub poll_interval_ms: Option<u64>,
}

/// The catalog a `notifications/mcptest/rotate_catalog` control message swaps
/// in (WOR-1471).
///
/// Declaring this is what makes the mock advertise `tools.listChanged: true` at
/// `initialize` and arms the one-shot mid-session rotation: when the control
/// notification arrives, the serve loop replaces the served tool catalog with
/// these tools and emits `notifications/tools/list_changed`. It exists so the
/// client/host conformance lane can test whether a client re-lists tools after
/// a server mutates its catalog (the cross-server-shadowing / rug-pull threat).
#[derive(Debug, Clone, Deserialize)]
pub struct ListChangedSpec {
    /// The replacement tool catalog served after the rotation fires.
    pub tools: Vec<MockTool>,
}

/// `InputRequiredResult` behavior for a mock tool (SEP-2322).
///
/// The first `tools/call` answers with these `requests` and `state`; the
/// client re-issues the call with `inputResponses` plus the echoed
/// `state` to get the tool's normal response.
#[derive(Debug, Clone, Deserialize)]
pub struct InputRequiredSpec {
    /// The `inputRequests` the server asks the client to fill in. A raw
    /// JSON array so a manifest can author the exact wire shape.
    pub requests: Value,
    /// The opaque `requestState` token the client must echo on the retry.
    pub state: String,
    /// Optional description surfaced at the `InputRequiredResult` level.
    #[serde(default)]
    pub description: Option<String>,
}

/// One resource the mock exposes via `resources/list` and `resources/read`.
#[derive(Debug, Clone, Deserialize)]
pub struct MockResource {
    /// Resource URI (`resources/read` matches on this).
    pub uri: String,
    /// Optional human-readable name echoed from `resources/list`.
    #[serde(default)]
    pub name: Option<String>,
    /// Optional MIME type advertised on list and read.
    #[serde(default, rename = "mime_type", alias = "mimeType")]
    pub mime_type: Option<String>,
    /// Text content returned by `resources/read`. Empty when unset.
    #[serde(default)]
    pub text: Option<String>,
}

/// One prompt the mock exposes via `prompts/list` and `prompts/get`.
#[derive(Debug, Clone, Deserialize)]
pub struct MockPrompt {
    /// Wire name of the prompt (`prompts/get` matches on this).
    pub name: String,
    /// Optional human-readable description echoed from `prompts/list`.
    #[serde(default)]
    pub description: Option<String>,
    /// Text of the single user message `prompts/get` returns. Empty when unset.
    #[serde(default)]
    pub text: Option<String>,
}

/// Parsed mock manifest.
///
/// Loaded from YAML (`mock_server.tools[]`) or from a `tools/list` JSON
/// snapshot (`tools[]`). The two shapes converge on the same vector so the
/// dispatcher does not care which file format produced it.
#[derive(Debug, Clone)]
pub struct MockManifest {
    /// Optional friendly name advertised on `initialize`. Defaults to
    /// `mcptest-mock` when the source file does not set one.
    pub name: String,
    /// Tools exposed by the mock.
    pub tools: Vec<MockTool>,
    /// Resources exposed via `resources/list` and `resources/read`.
    pub resources: Vec<MockResource>,
    /// Prompts exposed via `prompts/list` and `prompts/get`.
    pub prompts: Vec<MockPrompt>,
    /// The catalog a mid-session rotation swaps in, when declared (WOR-1471).
    /// `Some` makes the mock advertise `tools.listChanged: true` and arms the
    /// `notifications/mcptest/rotate_catalog` control message; `None` leaves
    /// the catalog static.
    pub list_changed: Option<ListChangedSpec>,
}

#[derive(Debug, Deserialize)]
struct YamlRoot {
    mock_server: YamlMockServer,
}

#[derive(Debug, Deserialize)]
struct YamlMockServer {
    #[serde(default)]
    name: Option<String>,
    #[serde(default)]
    tools: Vec<MockTool>,
    #[serde(default)]
    resources: Vec<MockResource>,
    #[serde(default)]
    prompts: Vec<MockPrompt>,
    #[serde(default, alias = "listChanged")]
    list_changed: Option<ListChangedSpec>,
}

#[derive(Debug, Deserialize)]
struct BaselineRoot {
    tools: Vec<MockTool>,
}

/// Load a mock manifest from disk.
///
/// Format is selected by extension: `.yml` / `.yaml` parse the
/// `mock_server.tools[]` shape; `.json` parses a bare `tools/list` snapshot.
/// The two formats converge on the same in-memory model.
pub fn load_manifest(path: &Path) -> Result<MockManifest> {
    let body = fs::read_to_string(path)
        .with_context(|| format!("failed to read mock manifest {}", path.display()))?;
    let ext = path
        .extension()
        .and_then(|s| s.to_str())
        .unwrap_or("")
        .to_ascii_lowercase();
    match ext.as_str() {
        "yml" | "yaml" => parse_yaml(&body)
            .with_context(|| format!("failed to parse YAML mock {}", path.display())),
        "json" => parse_baseline(&body)
            .with_context(|| format!("failed to parse JSON baseline {}", path.display())),
        _ => Err(anyhow!(
            "unsupported mock source extension `{}`; use .yaml or .json",
            ext
        )),
    }
}

/// Parse the YAML `mock_server.tools[]` manifest shape.
pub(crate) fn parse_yaml(body: &str) -> Result<MockManifest> {
    let root: YamlRoot = serde_yaml::from_str(body)?;
    Ok(MockManifest {
        name: root.mock_server.name.unwrap_or_else(default_name),
        tools: root.mock_server.tools,
        resources: root.mock_server.resources,
        prompts: root.mock_server.prompts,
        list_changed: root.mock_server.list_changed,
    })
}

/// Parse a bare `tools/list` JSON snapshot into a manifest.
pub(crate) fn parse_baseline(body: &str) -> Result<MockManifest> {
    let root: BaselineRoot = serde_json::from_str(body)?;
    Ok(MockManifest {
        name: default_name(),
        tools: root.tools,
        resources: Vec::new(),
        prompts: Vec::new(),
        list_changed: None,
    })
}

fn default_name() -> String {
    "mcptest-mock".to_string()
}
