//! CLI parser and dispatcher.
//!
//! Defines the clap-derive argument tree for `mcptest`, plus a [`dispatch`]
//! entry point that integration tests can call without spawning a subprocess.
//! Subcommand bodies live under [`handlers`] and each subcommand's clap
//! `Args` struct lives under [`args`]. This module stays a thin shell of
//! the top-level [`Cli`] struct, the [`Command`] enum, and the dispatch
//! match; [`GlobalArgs`] and [`Reporter`] live in [`global`].

use std::ffi::OsString;

use anyhow::Result;
use clap::{Parser, Subcommand};

pub mod args;
pub(crate) mod default_config;
pub mod exit_codes;
pub mod global;
pub mod handlers;
pub mod parsers;
pub(crate) mod reporters;

pub use args::*;
pub use global::{GlobalArgs, Reporter};
pub use parsers::parse_dollar_amount;

/// Help template that renders the subcommand list as task-oriented groups.
///
/// clap v4 cannot render multiple subcommand headings natively, so the
/// grouped block is literal text and the flat `{subcommands}` tag is left
/// out of the template. A parser test cross-checks every visible subcommand
/// against this block so a new command cannot ship without a group.
const GROUPED_HELP_TEMPLATE: &str = "\
{about-with-newline}
{usage-heading} {usage}

Commands:
  Start here:
    run            Run a YAML test suite against an MCP server
    init           Scaffold a new mcptest project in the current directory
    doctor         Diagnose the local environment and server connectivity
    validate       Validate the YAML config against the published JSON Schema

  Agent integration:
    mcp-server     Expose the mcptest engine to local agents over stdio
    skill          Print or install the test-mcp-server agent skill
    prompt         Print an LLM grounding prompt for writing mcptest YAML
    propose        Propose an expect block derived from observed responses

  Explore a server:
    inspect        Connect to a server and explore it in an interactive REPL
    tools          List tools, or call one with chaining primitives
    resources      List resources advertised by a server
    prompts        List prompts advertised by a server
    capabilities   Print the capability block returned during initialize
    discover       Introspect a server and scaffold a starter tests.yaml
    generate       Scaffold YAML test stubs from a live server's catalog
    mock           Spawn a YAML-driven stdio mock MCP server
    workbench      Open artifacts in a local self-contained HTML explorer

  Quality and security:
    compliance     Score a server against the compliance rubric
    conformance    Score a server against the SEP conformance corpus
    concurrency    Check a server stays correct under concurrent sessions
    security       Scan a tools/list snapshot, or drive the red-team lane
    posture        Score a deployment's runtime and gateway posture
    coverage       Compute coverage metrics for a server's surface
    fault-coverage Report which runtime fault families a suite exercises
    eval           Run quality evaluations against an MCP server
    ablation       Measure the task uplift the server's tools give the agent
    scenario       Replay multi-turn scenarios against a seeded world
    tool-bench     Mutation-backed tool-description selection benchmark
    judge          Certify a judge/jury against a labeled anchor set
    fuzz           Fuzz every tool with schema-derived malformed input
    lint           Scan suites and cassettes for deprecated MCP features
    schema-lint    Lint a tools/list snapshot's input schemas, with autofix
    scan           Statically scan an MCP config file for security risks
    model-compat   Capture, diff, or replay a model-compatibility baseline

  Reports and artifacts:
    report         Render a saved run as a reporter format
    import         Normalize an external benchmark/eval corpus into a suite
    ecosystem      Ingest public MCP ecosystem metadata into a local corpus
    interop        Convert promptfoo/DeepEval/mcp-eval suites to and from mcptest
    bakeoff        Render the competitive capability scorecard
    badge          Emit a CI status badge (shields JSON or SVG)
    diff           Diff two saved tools/list snapshots
    drift          Watch a server for schema + behavioral drift
    baseline       Update the timing baseline file from a run
    ledger         Emit or diff a session ledger
    evidence       Aggregate a run into a portable, signable evidence artifact
    sbom           Print the SBOM baked into the binary at build time
    manifest       Emit or diff a capability SBOM (tools by side-effect)
    matchers       List every matcher the YAML schema accepts
    policy         Evaluate a policy against saved artifacts
    schema         Emit the JSON Schema for the YAML config

  Plumbing:
    completions    Generate a shell completion script
    cache          Inspect or evict the local cache store
    login          Interactive OAuth 2.1 + PKCE login that caches a token
    migrate        Rewrite YAML suites toward an MCP spec revision
    record         Record live MCP traffic through a stdio proxy into a cassette
    distill        Convert a recorded cassette into a draft YAML suite
    pipe           Run a declarative multi-step tool-call pipeline
    web-bot-auth   Web Bot Auth helpers

Options:
{options}{after-help}";

/// Top-level CLI parser.
///
/// Held as a struct so global flags and the subcommand enum are wired in one
/// place. Integration tests build it via `Cli::parse_from`.
#[derive(Debug, Parser)]
#[command(
    name = "mcptest",
    bin_name = "mcptest",
    version,
    about = "Test runner for Model Context Protocol (MCP) servers",
    long_about = "mcptest validates MCP servers against YAML test suites. \
                  See https://mcptest.sh for documentation.",
    after_help = "Documentation:   https://mcptest.sh\n\
                  Examples:        https://github.com/soapbucket/mcptest/tree/main/examples\n\
                  Report a bug:    https://github.com/soapbucket/mcptest/issues",
    after_long_help = "Documentation:   https://mcptest.sh\n\
                       Reference:       https://mcptest.sh/cli-reference\n\
                       Agent tests:     https://mcptest.sh/models\n\
                       Examples:        https://github.com/soapbucket/mcptest/tree/main/examples\n\
                       Source:          https://github.com/soapbucket/mcptest\n\
                       Report a bug:    https://github.com/soapbucket/mcptest/issues",
    disable_help_subcommand = true,
    propagate_version = true,
    help_template = GROUPED_HELP_TEMPLATE
)]
pub struct Cli {
    /// Global flags that apply to every subcommand.
    #[command(flatten)]
    pub global: GlobalArgs,

    /// Subcommand to dispatch.
    #[command(subcommand)]
    pub command: Command,
}

/// Subcommand tree.
///
/// Each variant declares its own arg struct so subcommand-specific flags do
/// not pollute the global namespace, and routes to a live handler under
/// [`handlers`].
// `Run` carries the largest arg struct by a wide margin, which trips
// `large_enum_variant`. Boxing the variant is the usual fix, but clap's
// `Subcommand` derive needs the field to implement `Args`, which `Box<RunArgs>`
// does not. This enum is constructed exactly once per process at startup, so
// the size difference is irrelevant; allow the lint here rather than distort
// the arg structs.
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Subcommand)]
pub enum Command {
    /// Run the test suite (the primary command).
    Run(RunArgs),
    /// Scaffold a new mcptest project in the current directory.
    Init(InitArgs),
    /// Diagnose the local environment and server connectivity.
    Doctor(DoctorArgs),
    /// Validate the YAML config against the published JSON Schema.
    Validate(ValidateArgs),
    /// Emit the JSON Schema for the YAML config to stdout.
    Schema(SchemaArgs),
    /// Compute coverage metrics for a server's surface.
    Coverage(CoverageArgs),
    /// Report which runtime fault families a suite exercises.
    #[command(name = "fault-coverage")]
    FaultCoverage(FaultCoverageArgs),
    /// Render a saved run as a reporter format.
    Report(ReportArgs),
    /// Normalize an external benchmark or eval corpus into an agent suite.
    Import(ImportArgs),
    /// Ingest public MCP ecosystem metadata (MCPCorpus, registry, repo
    /// lists) into a deterministic local corpus, or select a fixture pack.
    Ecosystem(EcosystemArgs),
    /// Convert promptfoo, DeepEval, and mcp-eval suites to and from mcptest,
    /// or build a starter suite from a tools/list or registry export.
    Interop(InteropArgs),
    /// Render the dated competitive capability scorecard from the embedded,
    /// pinned competitor matrix.
    Bakeoff(BakeoffArgs),
    /// Emit a CI status badge (shields.io endpoint JSON or SVG) from a score
    /// or a saved report.
    Badge(BadgeArgs),
    /// Run quality evaluations against an MCP server.
    Eval(EvalArgs),
    /// Replay multi-turn scenarios against a seeded hidden world.
    Scenario(ScenarioArgs),
    /// Mutation-backed tool-description benchmark: selection impact + smells.
    ToolBench(ToolBenchArgs),
    /// Generate shell completion script for the chosen shell.
    Completions(CompletionsArgs),
    /// Diff two saved `tools/list` snapshots.
    Diff(DiffArgs),
    /// Watch a server for schema + behavioral drift against a baseline.
    Drift(DriftArgs),
    /// Introspect an MCP server and scaffold a starter `tests.yaml`.
    Discover(DiscoverArgs),
    /// Record live MCP traffic through a stdio proxy into a cassette
    /// (point your MCP client at `mcptest record --target <real server>`).
    Record(RecordArgs),
    /// Convert a recorded cassette into a draft YAML suite, one test per
    /// recorded tool call.
    Distill(DistillArgs),
    /// Connect to a server and explore it in an interactive REPL.
    Inspect(InspectCliArgs),
    /// Scan YAML suites and cassettes for MCP 2026-07-28 deprecated-feature
    /// usage (Roots / Sampling / Logging, `tasks/list`, `-32002`).
    Lint(LintArgs),
    /// Lint the input schemas in a saved `tools/list` snapshot for
    /// under-constrained shapes (`SCH-NNN`), with a `--fix` autofix.
    SchemaLint(SchemaLintArgs),
    /// Statically scan an MCP config file (`mcp.json`,
    /// `claude_desktop_config.json`) for security risks without launching a
    /// server: hardcoded secrets, broad filesystem access, missing credentials,
    /// and auto-approved tools, with a per-server risk score.
    Scan(ScanArgs),
    /// Rewrite YAML suites toward an MCP spec revision: annotate
    /// deprecated features with TODO comments and apply the mechanical
    /// `-32002` legacy error code rewrite.
    Migrate(MigrateArgs),
    /// Run as an IPC co-process for the native SDKs.
    ///
    /// Hidden from `--help`: the language SDKs spawn it; humans never type
    /// it. It still parses and `mcptest exec --help` still works.
    #[command(hide = true)]
    Exec(ExecArgs),
    /// Capture, diff, or replay a model-compatibility baseline.
    ModelCompat(ModelCompatArgs),
    /// Spawn a YAML-driven stdio mock MCP server.
    Mock(MockArgs),
    /// Stdio MCP server exposing the mcptest engine to local agents
    /// (Claude Code, Cursor, mcp-inspector).
    #[command(name = "mcp-server")]
    McpServer(McpServerArgs),
    /// List tools, or call one with chaining primitives.
    Tools(ToolsCliArgs),
    /// Fuzz every tool a server exposes with schema-derived malformed input.
    Fuzz(FuzzArgs),
    /// List resources advertised by an MCP server.
    Resources(IntrospectCliArgs),
    /// List prompts advertised by an MCP server.
    Prompts(IntrospectCliArgs),
    /// Print the capability block returned during `initialize`.
    Capabilities(IntrospectCliArgs),
    /// Scaffold YAML test stubs from a live server's catalog.
    Generate(GenerateArgs),
    /// Interactive OAuth 2.1 + PKCE login that caches a token for later runs.
    Login(LoginArgs),
    /// Score a server against the compliance rubric.
    Compliance(ComplianceArgs),
    /// Open N concurrent sessions against a server and check it stays correct
    /// under concurrency (liveness, determinism, isolation). Not a load test.
    Concurrency(ConcurrencyArgs),
    /// Run each single-prompt agent test with the server's tools attached and
    /// withheld, and report the task-success uplift the tools provide.
    Ablation(AblationArgs),
    /// Update the timing baseline file the
    /// `mcptest run --check-baseline` gate reads.
    Baseline(BaselineArgs),
    /// Certify a judge/jury against a labeled anchor set before its verdict
    /// may gate CI.
    Judge(JudgeArgs),
    /// Diff a run's session ledger against a baseline trajectory, or emit a
    /// ledger from a saved agent envelope.
    Ledger(LedgerArgs),
    /// Score a server against the vendored SEP conformance corpus,
    /// refresh the corpus from upstream, or list the check ids it
    /// carries.
    Conformance(ConformanceArgs),
    /// Print a copy paste ready LLM prompt for writing mcptest YAML tests.
    Prompt(PromptArgs),
    /// Call one tool on a live server (twice for read-only tools) and print
    /// a proposed expect block derived from the observed responses.
    Propose(ProposeArgs),
    /// Inspect or evict the local cache store.
    Cache(CacheArgs),
    /// Run a declarative multi-step tool-call pipeline.
    Pipe(PipeArgs),
    /// Evaluate a policy against saved artifacts (a local governance gate).
    Policy(PolicyArgs),
    /// Scan a tools/list snapshot, or drive the live red-team lane with
    /// `security redteam`.
    Security(SecurityArgs),
    /// Score an MCP deployment's runtime and gateway posture (isolation,
    /// egress, identity, registry provenance, telemetry, audit).
    Posture(PostureArgs),
    /// Web Bot Auth helpers, including publishing the public-key directory
    /// document.
    #[command(name = "web-bot-auth")]
    WebBotAuth(WebBotAuthArgs),
    /// Print the CycloneDX Software Bill of Materials baked into the
    /// binary at build time, list licenses, or verify the embedded blob
    /// has not been tampered with.
    Sbom(SbomArgs),
    /// Emit or diff a capability SBOM: a server's tools classified by
    /// side-effect into a stable, diffable manifest (capability drift).
    Manifest(ManifestArgs),
    /// Aggregate a run's metadata into a portable, signable evidence
    /// artifact, or verify one with `evidence verify`.
    Evidence(EvidenceArgs),
    /// List every matcher the YAML schema accepts, with one example each.
    Matchers(MatchersArgs),
    /// Gather saved artifacts (or a live capture) into a self-contained,
    /// local HTML workbench. No account, no network, no telemetry.
    Workbench(WorkbenchArgs),
    /// Print the packaged "test-mcp-server" agent skill, or install it
    /// (and its companion subagent) with `--install`.
    Skill(SkillArgs),
}

/// Parse arguments and execute the requested subcommand.
///
/// Returns the desired process exit code. clap's own errors (including
/// `--help` and `--version`) take their printing path via [`clap::Error::exit`]
/// so the output matches what users expect from any clap binary.
pub fn dispatch<I, T>(args: I) -> Result<i32>
where
    I: IntoIterator<Item = T>,
    T: Into<OsString> + Clone,
{
    let cli = match Cli::try_parse_from(args) {
        Ok(cli) => cli,
        Err(err) => err.exit(),
    };

    match cli.command {
        Command::Run(ref args) => handlers::run_command(&cli, args),
        Command::Init(args) => handlers::init_command(&args, &cli.global),
        Command::Doctor(args) => handlers::doctor_command(&cli.global, &args),
        Command::Validate(ref args) => handlers::validate_command(&cli.global, args),
        Command::Schema(args) => crate::commands::run_schema(&args.schema_version),
        Command::Coverage(args) => handlers::coverage_command(&args),
        Command::FaultCoverage(args) => handlers::fault_coverage_command(&args),
        Command::Report(args) => handlers::report_command(args, &cli.global),
        Command::Import(args) => handlers::import_command(args),
        Command::Ecosystem(args) => handlers::ecosystem_command(args),
        Command::Interop(args) => handlers::interop_command(args),
        Command::Bakeoff(args) => handlers::bakeoff_command(&args),
        Command::Badge(args) => handlers::badge_command(args),
        Command::Eval(args) => handlers::eval_command(&cli.global, &args),
        Command::Scenario(args) => handlers::scenario_command(args),
        Command::ToolBench(args) => handlers::tool_bench_command(args),
        Command::Completions(args) => handlers::run_completions(args.shell),
        Command::Diff(args) => handlers::diff_command(args),
        Command::Drift(args) => handlers::drift_command(args),
        Command::Discover(args) => handlers::discover_command(&args),
        Command::Record(ref args) => handlers::record_command(&cli.global, args),
        Command::Distill(args) => handlers::distill_command(args),
        Command::Inspect(args) => handlers::inspect_command(args),
        Command::Lint(args) => handlers::lint_command(args),
        Command::SchemaLint(args) => handlers::schema_lint_command(args),
        Command::Scan(args) => handlers::scan_command(args),
        Command::Migrate(args) => handlers::migrate_command(args),
        Command::Exec(args) => handlers::exec_command(&cli.global, args),
        Command::ModelCompat(args) => handlers::model_compat_command(args),
        Command::Mock(args) => handlers::mock_command(&cli.global, args),
        Command::McpServer(args) => handlers::mcp_server_command(args),
        Command::Tools(args) => handlers::tools_command(args),
        Command::Fuzz(ref args) => handlers::fuzz_command(&cli.global, args),
        Command::Resources(args) => {
            handlers::introspect_command(args, handlers::IntrospectKind::Resources)
        }
        Command::Prompts(args) => {
            handlers::introspect_command(args, handlers::IntrospectKind::Prompts)
        }
        Command::Capabilities(args) => {
            handlers::introspect_command(args, handlers::IntrospectKind::Capabilities)
        }
        Command::Generate(args) => handlers::generate_command(args),
        Command::Login(args) => handlers::login_command(&cli.global, args),
        Command::Compliance(args) => handlers::compliance_command(&cli.global, args),
        Command::Concurrency(args) => handlers::concurrency_command(args),
        Command::Ablation(args) => handlers::ablation_command(args),
        Command::Baseline(args) => handlers::baseline_command(args),
        Command::Judge(args) => handlers::judge_command(args),
        Command::Ledger(args) => handlers::ledger_command(args),
        Command::Conformance(args) => handlers::conformance_command(args),
        Command::Prompt(args) => handlers::prompt_command(args),
        Command::Propose(args) => handlers::propose_command(args),
        Command::Cache(args) => handlers::cache_command(args),
        Command::Pipe(args) => handlers::pipe_command(args),
        Command::Policy(args) => handlers::policy_command(args),
        Command::Security(args) => handlers::security_command(args, &cli.global),
        Command::Posture(args) => handlers::posture_command(&args),
        Command::WebBotAuth(args) => handlers::web_bot_auth_command(args),
        Command::Sbom(args) => handlers::sbom_command(args),
        Command::Manifest(args) => handlers::manifest_command(args),
        Command::Evidence(args) => handlers::evidence_command(args),
        Command::Matchers(args) => handlers::matchers_command(&args),
        Command::Workbench(args) => handlers::workbench_command(&args),
        Command::Skill(args) => handlers::skill_command(args),
    }
}

#[cfg(test)]
mod tests;
