//! Fault-injection declarations for the `recovery:` matcher.
//!
//! A suite declares named faults under a top-level `faults:` block and an agent
//! test opts into them with `inject: [name, ...]`. The fault vocabulary mirrors
//! the mock server's `--fault` kinds (hang, wedged, slow, recover_after) so a
//! reader who knows one knows the other, but these types deliberately do not
//! reuse `mcptest_mcp_server::Fault`: the recovery executor synthesizes the
//! outcome in virtual time (no wall-clock sleep, no launched mock), so a core
//! type with no server dependency is the right shape.

/// Which fault to synthesize for a matching tool call.
///
/// `Hang` and `Wedged` are the canonical dead-but-connected fault (the call
/// never returns). `Slow` answers after `delay_ms`; a delay at or beyond the
/// detection budget reads as unresponsive. `RecoverAfter` hangs the first
/// `failures` calls, then answers, so an agent's retry path is exercised.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FaultKind {
    /// The call never returns (reachable but unresponsive).
    Hang,
    /// The backend is wedged; the agent-visible effect matches [`FaultKind::Hang`].
    Wedged,
    /// The call answers after `delay_ms` milliseconds.
    Slow {
        /// Injected delay before the (normal) response, milliseconds.
        delay_ms: u64,
    },
    /// The call hangs the first `failures` times, then answers normally.
    RecoverAfter {
        /// Number of calls that hang before the server recovers.
        failures: u32,
    },
}

/// Which calls a fault applies to: a named tool, a whole server, or both.
///
/// A `None` field matches anything, so `{ server: x }` faults every tool on
/// `x` and `{ tool: t }` faults `t` on any server. A target with both fields
/// `None` matches every call, which the parser rejects so a fault always names
/// something.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FaultTarget {
    /// Tool name the fault applies to, or `None` for any tool.
    pub tool: Option<String>,
    /// Server key the fault applies to, or `None` for any server.
    pub server: Option<String>,
}

impl FaultTarget {
    /// True when a call to `tool` on `server` matches this target.
    pub fn matches(&self, tool: &str, server: &str) -> bool {
        self.tool.as_deref().is_none_or(|t| t == tool)
            && self.server.as_deref().is_none_or(|s| s == server)
    }
}

/// One named fault from the suite-level `faults:` block.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FaultSpec {
    /// Unique name an agent test references via `inject:`.
    pub name: String,
    /// Which calls the fault applies to.
    pub target: FaultTarget,
    /// What the fault does.
    pub kind: FaultKind,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn target_none_fields_match_anything() {
        let any_tool = FaultTarget {
            tool: None,
            server: Some("backend".into()),
        };
        assert!(any_tool.matches("search", "backend"));
        assert!(any_tool.matches("fetch", "backend"));
        assert!(!any_tool.matches("search", "other"));

        let any_server = FaultTarget {
            tool: Some("search".into()),
            server: None,
        };
        assert!(any_server.matches("search", "alpha"));
        assert!(!any_server.matches("fetch", "alpha"));
    }
}
