//! Typed data for the multi-run agent-test gates.
//!
//! These small structs (`ToolSelectionFloor`, `StabilityGate`,
//! `EqualFunctionGate`, `DistractorGate`) are the data side of the agent-test
//! gate blocks parsed in [`super::agent_gates_parse`]. They live in their own
//! module so `suite/types.rs` stays under the module-size ceiling. The parent
//! re-exports them so external paths such as
//! `mcptest_core::suite::EqualFunctionGate` keep resolving unchanged.

use super::Assertion;

/// Cross-run tool-selection floor for a multi-run agent test.
///
/// This expresses the tokens-vs-tool-selection tradeoff as a gate: a
/// single run can pick the right tool by luck, so the floor asserts a
/// minimum *rate* across `runs:` independent runs, optionally capping
/// the per-run token spend so a model cannot buy correctness with an
/// unbounded budget. The CLI computes the rate and gates on it.
#[derive(Debug, Clone, PartialEq)]
pub struct ToolSelectionFloor {
    /// Tool name the model is expected to select. A run "selects" the
    /// tool when this name appears in any `tool_calls[].name` entry.
    pub expected_tool: String,
    /// Minimum fraction of runs (0.0 to 1.0) that must select the tool
    /// for the floor to pass.
    pub min_selection_rate: f64,
    /// Optional per-run cap on `conversation.tokens.total`. `None` means
    /// no token budget; a run is always "within budget" then. When set,
    /// every run must stay at or under the cap for the floor to pass.
    pub max_total_tokens: Option<u32>,
}

/// Within-session stability gate for a multi-run agent test.
///
/// A single run can look stable by luck, so this gate aggregates the
/// within-session stability sub-scores ([`crate::eval::stability`]) across the
/// `runs:` independent runs and asserts on the aggregate. It exposes three
/// targets the `expect:` gates with the standard matchers:
///
/// - `stability.score`: the mean per-run weakest sub-score across the runs.
/// - `stability.weakest_score`: the single worst run's weakest sub-score.
/// - `stability.variance`: how much the per-run weakest sub-scores varied.
///
/// The gate only makes sense over a sample, so the parser rejects it on an
/// agent test that is not configured to run more than once (`runs: 1`).
#[derive(Debug, Clone, PartialEq)]
pub struct StabilityGate {
    /// Assertions over the `stability.*` targets. Empty means "apply the
    /// default": `stability.weakest_score >= 0.5`.
    pub expect: Vec<Assertion>,
}

/// Structural tool-edge coverage gate over a declared edge set (WOR-1242).
///
/// Declares the tools a run should exercise (allowed), must never call
/// (restricted), and the agent hand-offs it should perform (delegation), then
/// gates the `edges.*` targets folded from the run trace. An empty `expect:`
/// applies the default gate: no call to a restricted tool.
#[derive(Debug, Clone, PartialEq)]
pub struct ToolEdgesGate {
    /// The declared allowed, restricted, and delegation edges.
    pub set: crate::eval::tool_edges::EdgeSet,
    /// Assertions over the `edges.*` targets. Empty means the default gate.
    pub expect: Vec<Assertion>,
}

/// Objective multi-server tool-selection F1 gate over equal-function sets
/// (MSC-Bench).
///
/// Why this gate exists: judging whether an agent picked the right tool usually
/// needs a model in the loop. MSC-Bench removes the model by grouping
/// interchangeable tools into equal-function sets (equivalence classes): a tool
/// choice is correct when it lands in the right class, which makes precision,
/// recall, and F1 deterministic from a recorded trace with zero LLM calls. The
/// CLI folds the `runs:` traces into three assertable targets the `expect:`
/// gates with the standard matchers:
///
/// - `tool_selection.f1`: micro-averaged F1 percent across the runs.
/// - `tool_selection.precision`: micro-averaged precision percent.
/// - `tool_selection.recall`: micro-averaged recall percent.
///
/// All three are integer percents in `0..=100`, so a floor is a plain
/// `minimum:` on the percent. An empty or omitted `expect:` applies the default
/// gate `tool_selection.f1 >= 50`.
#[derive(Debug, Clone, PartialEq)]
pub struct EqualFunctionGate {
    /// The declared equal-function sets, each a named class of interchangeable
    /// `server.tool` ids. Always non-empty after parsing: a gate with no
    /// classes cannot score anything, so the parser rejects it.
    pub classes: Vec<crate::eval::tool_selection_f1::EqualFunctionSet>,
    /// Assertions over the `tool_selection.*` targets. Empty means "apply the
    /// default": `tool_selection.f1 >= 50`.
    pub expect: Vec<Assertion>,
}

/// Tool-surface token-efficiency gate for a multi-run agent test (WOR-1366).
///
/// Why this gate exists: a fat tool catalog can score a fine selection F1 while
/// burning input tokens on every call, so accuracy alone hides an inefficient
/// surface. This gate scores the same equal-function selection F1 as
/// [`EqualFunctionGate`], then folds in the serialized tool-surface token count
/// and the run cost to expose the per-correct price of the catalog. The CLI
/// computes the assertable `token_efficiency.*` targets (`f1`,
/// `tokens_per_correct`, `cost_per_correct`, `grade`, ...) with no extra model
/// call: F1 and correct selections come from the recorded trace, the
/// tool-surface tokens from the live catalog, and the cost from the run.
#[derive(Debug, Clone, PartialEq)]
pub struct TokenEfficiencyGate {
    /// The declared equal-function sets the selection F1 scores against, the
    /// same shape [`EqualFunctionGate`] uses. Always non-empty after parsing.
    pub classes: Vec<crate::eval::tool_selection_f1::EqualFunctionSet>,
    /// Assertions over the `token_efficiency.*` targets. Empty means "apply the
    /// default": `token_efficiency.f1 >= 50`.
    pub expect: Vec<Assertion>,
}

/// Name-free discovery declaration on an agent test (MCP-Atlas).
///
/// MCP-Atlas distinguishes scenarios whose prompt names the tool to call from
/// scenarios whose prompt states only the user's intent, forcing the agent to
/// discover the tool itself. Marking a test `name_free` is the author's
/// declaration that the prompt names no tools or servers, so the discovered
/// tool path is scored against equal-function classes (via the
/// `equal_function_sets:` gate) rather than against a named expected tool.
///
/// WHY a struct and not a bare bool: it leaves room to attach a rationale or
/// future discovery options without a breaking change, mirroring how the other
/// gate blocks are objects.
#[derive(Debug, Clone, PartialEq)]
pub struct NameFreeDiscovery {
    /// True when the prompt is intent-only and names no tools or servers.
    pub name_free: bool,
}

/// Orchestration diagnostics gate over the five MCP-Atlas sub-scores.
///
/// Why this gate exists: a run can reach the right capability yet waste calls,
/// call the right tool with malformed arguments, or fail once and never
/// recover. Collapsing that into one number hides where the agent went wrong,
/// so MCP-Atlas scores five independent axes. The CLI folds the run trace into
/// the assertable `orchestration.*` targets and gates on them with no model in
/// the loop:
///
/// - `orchestration.discovery`: reached the right equal-function class(es).
/// - `orchestration.parameterization`: fraction of calls with non-empty args.
/// - `orchestration.syntax`: fraction of calls that are structurally valid.
/// - `orchestration.error_recovery`: fraction of errored calls later recovered.
/// - `orchestration.efficiency`: optimal-over-observed call ratio, capped.
///
/// Each axis is an integer percent in `0..=100`, so a floor is a plain
/// `minimum:` on the percent (see [`crate::eval::orchestration`]).
#[derive(Debug, Clone, PartialEq)]
pub struct OrchestrationGate {
    /// Assertions over the `orchestration.*` targets. Empty after parsing means
    /// the gate only computes and reports the sub-scores without gating, so a
    /// suite can surface the diagnostics without asserting a floor.
    pub expect: Vec<Assertion>,
}

/// Recovery gate for an agent run against an injected fault.
///
/// Why this gate exists: the bundled lanes test servers that respond. This one
/// tests what an agent does when a tool is reachable but unresponsive (the
/// dead-but-connected fault). The agent test names a fault under `inject:`, and
/// the executor synthesizes the unresponsive call in virtual time, scoring the
/// run with [`crate::eval::recovery`]. The three fields drive pass/fail:
///
/// - `max_detection_ms`: the per-call timeout budget. A correctly configured
///   agent should give up on a hung call within this, not block forever.
/// - `max_recovery_ms`: how long the whole run may take to recover (succeed
///   another way, fall back, or surface a clean error). `None` leaves recovery
///   time unbounded, so the gate only requires that the agent recovered at all.
/// - `require_clean_timeout`: when true, also require that the agent timed out
///   cleanly within its detection budget rather than hanging past it.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RecoveryGate {
    /// Per-call detection budget in milliseconds.
    pub max_detection_ms: u64,
    /// Optional cap on total recovery time in milliseconds. `None` leaves it
    /// unbounded.
    pub max_recovery_ms: Option<u64>,
    /// Require a clean timeout within the detection budget.
    pub require_clean_timeout: bool,
}

/// Distractor-tool / tool-overload gate for a multi-run agent test
/// (MCPAgentBench, MCP-Atlas).
///
/// Why this gate exists: a real candidate list is padded with irrelevant or
/// near-duplicate tools, and a capable agent must still pick the correct one
/// amid the noise. This gate declares the distractor injection
/// ([`crate::eval::distractors::DistractorSpec`]) plus the correct tool ids, and
/// the CLI scores the recorded selection objectively (exact membership, no model
/// call) into two assertable targets the `expect:` gates with the standard
/// matchers:
///
/// - `distractors.accuracy`: selection accuracy percent (chose-correct over the
///   in-scope chosen tools).
/// - `distractors.chose_distractor`: how many injected distractors were chosen.
///
/// `accuracy` is an integer percent in `0..=100`, so a floor is a plain
/// `minimum:`. An empty or omitted `expect:` applies the default gate
/// `distractors.accuracy >= 50`.
#[derive(Debug, Clone, PartialEq)]
pub struct DistractorGate {
    /// How distractor tools are injected into the candidate list (count plus
    /// catalog or near-duplicate source, with an optional complexity tag).
    pub spec: crate::eval::distractors::DistractorSpec,
    /// The correct tool ids for the task. Always non-empty after parsing: a
    /// gate with no correct tool cannot score selection accuracy, so the parser
    /// rejects it.
    pub correct: Vec<String>,
    /// Assertions over the `distractors.*` targets. Empty means "apply the
    /// default": `distractors.accuracy >= 50`.
    pub expect: Vec<Assertion>,
}

/// Context-rot knee probe gate for an agent test (context-rot sweep).
///
/// Why this gate exists: a single accuracy number at one catalog size hides the
/// cliff where a long candidate list starts to confuse the model. This gate
/// sweeps the served tool-catalog size across `levels:` (by injecting that many
/// distractor tools at each level), re-runs the agent per level, scores
/// selection accuracy per level, and feeds the `(context_tokens, quality)`
/// points to [`crate::eval::context_rot::summarize`]. The CLI then gates the
/// resulting `context_rot.*` targets (`knee_detected`, `max_degradation`,
/// `knee_tokens`, ...) with no model in the scoring loop. Live-run only: the
/// sweep re-runs the agent at each level, so a cassette replay (which carries a
/// single catalog) does not fire it.
#[derive(Debug, Clone, PartialEq)]
pub struct ContextRotProbeGate {
    /// Distractor counts to sweep, each one a level of served-catalog size. Each
    /// level injects that many distractors, re-runs the agent, and contributes
    /// one `(context_tokens, quality)` point to the curve. Always non-empty
    /// after parsing: a sweep with no levels has no curve to fold.
    pub levels: Vec<u32>,
    /// Where the swept distractor tools come from (the bundled catalog or
    /// near-duplicate variants), shared across every level so only the count
    /// changes as the catalog grows.
    pub source: crate::eval::distractors::DistractorSource,
    /// The correct tool ids for the task, scored against the chosen tools at
    /// every level to yield the per-level quality. Always non-empty after
    /// parsing: a gate with no correct tool cannot score selection.
    pub correct: Vec<String>,
    /// Assertions over the `context_rot.*` targets. Empty means "report the
    /// curve without gating", so a suite can surface the knee diagnostics
    /// without asserting a floor (a degradation gate is a deliberate choice).
    pub expect: Vec<Assertion>,
}

/// Golden-path efficiency gate for a multi-run agent test.
///
/// Where the trajectory gate asks "did the right calls happen?", the golden
/// path asks "did they happen efficiently?". It declares the ideal call
/// sequence plus a waste-penalty policy and the CLI scores the recorded
/// trajectory against it with no model in the loop, into assertable targets:
///
/// - `golden_path.penalty`: efficiency multiplier in `0.0..=1.0` (1.0 = no
///   penalized waste).
/// - `golden_path.passed`: `1` when penalty is 1.0, else `0`.
/// - `golden_path.extra_steps` / `backtracks` / `repeated_tools`: the waste
///   counts behind the penalty.
///
/// An empty or omitted `expect:` applies the default gate
/// `golden_path.passed >= 1` (strict: no penalized waste).
#[derive(Debug, Clone, PartialEq)]
pub struct GoldenPathGate {
    /// The ideal call sequence and the waste-penalty policy.
    pub path: crate::eval::GoldenPath,
    /// Assertions over the `golden_path.*` targets. Empty means "apply the
    /// default": `golden_path.passed >= 1`.
    pub expect: Vec<Assertion>,
}

/// Trajectory validation gate for a multi-run agent test.
///
/// Declares the expected call sequence and the match mode (strict,
/// subsequence, unordered, subset, superset) and the CLI validates the
/// recorded trajectory against it with no model in the loop, into assertable
/// targets:
///
/// - `trajectory.passed`: `1` when every expected call matched, else `0`.
/// - `trajectory.mismatch_count`: number of mismatched positions.
///
/// An empty or omitted `expect:` applies the default gate
/// `trajectory.passed >= 1`.
#[derive(Debug, Clone, PartialEq)]
pub struct TrajectoryGate {
    /// The expected calls plus the match mode.
    pub expected: crate::eval::ExpectedTrace,
    /// Assertions over the `trajectory.*` targets. Empty means "apply the
    /// default": `trajectory.passed >= 1`.
    pub expect: Vec<Assertion>,
}

/// Four-axis trajectory scorecard gate for a multi-run agent test (WOR-1368).
///
/// Extends the trajectory vocabulary with two structural axes scored from the
/// recorded call sequence with no model in the loop:
///
/// - `trajectory.dependency_satisfaction`: percent of declared producer ->
///   consumer edges where the consumer call occurred after the producer.
/// - `trajectory.order_satisfaction`: percent of declared ordered pairs where
///   the first call precedes the second.
///
/// Only the positional form of dependency is checkable from a trace (the
/// consumer call came after the producer); true data-flow provenance is not in
/// the envelope, so the suite declares the edges. An empty or omitted `expect:`
/// applies the default gate requiring both axes at 100.
#[derive(Debug, Clone, PartialEq)]
pub struct TrajectoryAxesGate {
    /// Declared producer -> consumer call-name pairs.
    pub dependencies: Vec<crate::eval::DependencyEdge>,
    /// Declared ordered call-name pairs `(first, second)`.
    pub order: Vec<(String, String)>,
    /// Assertions over the `trajectory.dependency_satisfaction` and
    /// `trajectory.order_satisfaction` targets. Empty means "apply the
    /// default": both axes at 100.
    pub expect: Vec<Assertion>,
}
