//! Arguments for `mcptest security`.

use std::path::PathBuf;

use clap::{Args, Subcommand, ValueEnum};
use mcptest_core::security::SecuritySeverity;

use super::redteam::RedteamArgs;

/// Run the deterministic security checks against a tools/list snapshot, or the
/// live red-team lane with `security redteam`.
///
/// The deterministic lanes (surface, toxic-flow, and the relational namespace
/// and integrity lanes when their inputs are present) own the pass/fail
/// verdict. The advisory LLM-judge lane runs only with `--model` and never
/// changes the verdict or exit code (ADR 0041). The `redteam` subcommand is a
/// separate, advisory-only lane (ADR 0041) that drives a live server.
///
/// `subcommand_negates_reqs` lets `security redteam ...` run without the
/// positional snapshot, so the snapshot scan and the live lane share one
/// command without either requiring the other's inputs.
#[derive(Debug, Args)]
#[command(subcommand_negates_reqs = true)]
pub struct SecurityArgs {
    /// Path to a `tools/list`-style JSON snapshot (may carry `tools`,
    /// `prompts`, and `resources` arrays, plus an optional `servers` array for
    /// the multi-server namespace lane). Required unless a subcommand is given.
    #[arg(required = true)]
    pub snapshot: Option<PathBuf>,

    /// Output format.
    #[arg(long, value_enum, default_value_t = SecurityFormat::Pretty)]
    pub format: SecurityFormat,

    /// Exit non-zero when any deterministic finding is at or above this
    /// severity. Advisory findings never affect this gate.
    #[arg(long, value_enum, default_value_t = SecurityFailOn::High)]
    pub fail_on: SecurityFailOn,

    /// Path to a pinned baseline manifest JSON to diff the snapshot against.
    /// Enables the integrity lane (rug-pull and schema-drift detection).
    #[arg(long)]
    pub baseline: Option<PathBuf>,

    /// Path to a waiver file (`.mcptest-waivers.yaml`). Accepted findings are
    /// excluded from `--fail-on` but still shown; expired and stale waivers are
    /// reported.
    #[arg(long, value_name = "PATH")]
    pub waivers: Option<PathBuf>,

    /// Model id (for example `claude-sonnet-4-5`) that enables the advisory
    /// LLM-judge lane. The provider is resolved from the environment the same
    /// way agent and llm-judge runs resolve it. Advisory findings are reported
    /// separately and never change the verdict or exit code.
    #[arg(long)]
    pub model: Option<String>,

    /// Optional subcommand. When present, runs that lane instead of the
    /// snapshot scan.
    #[command(subcommand)]
    pub command: Option<SecurityCommand>,
}

/// Subcommands under `mcptest security`.
#[derive(Debug, Subcommand)]
pub enum SecurityCommand {
    /// Drive the live Layer C1 red-team corpus against a running server and
    /// report per-model exploitability (advisory only, never the verdict).
    Redteam(RedteamArgs),
    /// Fold an external scanner's output (SARIF or Snyk agent-scan JSON) into a
    /// unified security report, deduped against the bundled catalog.
    Import(SecurityImportArgs),
    /// Scan recorded cassettes for runtime output poisoning: prompt injection
    /// (SEC-063) and hidden unicode (SEC-064) in tool-call results and error
    /// bodies. Deterministic and offline, so it is safe to gate CI on.
    Cassette(SecurityCassetteArgs),
    /// Map MCP tool handlers in TypeScript/Python/Rust source to risky sinks
    /// and emit advisory findings, SARIF with source locations, and a
    /// reproducible PoC suite (gated behind explicit consent).
    Proof(SecurityProofArgs),
    /// Scan agent skill files (SKILL.md) for prompt injection and other
    /// instruction-text attacks, reusing the surface text checks.
    Skill(SecuritySkillArgs),
}

/// Arguments for `mcptest security skill`.
#[derive(Debug, Args)]
pub struct SecuritySkillArgs {
    /// One or more agent skill files (SKILL.md) to scan.
    #[arg(value_name = "PATH", required = true)]
    pub paths: Vec<PathBuf>,

    /// Emit findings as a JSON array instead of the pretty table.
    #[arg(long)]
    pub json: bool,

    /// Exit non-zero when any finding is at or above this severity.
    #[arg(long, value_enum)]
    pub fail_on: Option<SecurityFailOn>,
}

/// Arguments for `mcptest security proof`.
///
/// A conservative source scanner: it resolves tool handlers and maps them to
/// risky sink categories. Every finding is advisory; a sink near a handler is a
/// lead, not a proof. Generating the PoC suite (the path to dynamic
/// confirmation against a disposable target) requires explicit consent via
/// `--authorize`.
#[derive(Debug, Args)]
pub struct SecurityProofArgs {
    /// Source file or directory to scan.
    #[arg(value_name = "SRC")]
    pub src: PathBuf,

    /// Output format for the advisory report.
    #[arg(long, value_enum, default_value_t = SecurityProofFormat::Pretty)]
    pub format: SecurityProofFormat,

    /// Exit non-zero when any advisory finding is at or above this severity.
    /// Off by default: static leads should inform, not gate.
    #[arg(long, value_enum)]
    pub fail_on: Option<SecurityFailOn>,

    /// Write a reproducible mcptest PoC suite here. Requires `--authorize` and
    /// `--target`. The suite runs negative-path probes against a disposable,
    /// sandboxed copy of the server you are authorized to test.
    #[arg(long, value_name = "PATH", requires = "authorize", requires = "target")]
    pub poc: Option<PathBuf>,

    /// The disposable target command the generated PoC suite runs against.
    #[arg(long, value_name = "CMD")]
    pub target: Option<String>,

    /// Explicit acknowledgement that you are authorized to probe the target and
    /// will run the PoC only against a disposable, sandboxed copy.
    #[arg(long)]
    pub authorize: bool,
}

/// Output formats for `mcptest security proof --format`.
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, ValueEnum)]
#[value(rename_all = "kebab-case")]
pub enum SecurityProofFormat {
    /// Human-friendly table (default).
    #[default]
    Pretty,
    /// One JSON object: the source-proof report.
    Json,
    /// SARIF 2.1.0 with source locations.
    Sarif,
}

/// Arguments for `mcptest security cassette`.
///
/// The surface scan covers tool DEFINITIONS; this covers the other half, the
/// tool OUTPUTS, by replaying the recorded result and error payloads through the
/// same injection and hidden-unicode detectors. A cassette is a deterministic
/// artifact, so a poisoned result that slipped past the definition scan is
/// caught reproducibly here.
#[derive(Debug, Args)]
pub struct SecurityCassetteArgs {
    /// Recorded cassette JSON files to scan (at least one, repeatable).
    #[arg(required = true, value_name = "FILE")]
    pub cassettes: Vec<PathBuf>,

    /// Output format.
    #[arg(long, value_enum, default_value_t = SecurityFormat::Pretty)]
    pub format: SecurityFormat,

    /// Exit non-zero when any finding is at or above this severity.
    #[arg(long, value_enum, default_value_t = SecurityFailOn::High)]
    pub fail_on: SecurityFailOn,
}

/// Arguments for `mcptest security import`.
///
/// mcptest owns the ingest, not the scan: it normalizes a scanner you already
/// run into the finding vocabulary and dedups it against the bundled catalog so
/// a combined report counts an overlap once. Each `--sarif`, `--snyk`, and
/// `--supplement` flag is repeatable, so several scanner outputs fold into one
/// report. An optional `--snapshot` also runs the bundled deterministic lanes
/// and folds the imports in beside them.
#[derive(Debug, Args)]
pub struct SecurityImportArgs {
    /// SARIF 2.1.0 file from an external scanner (repeatable). The scanner name
    /// is read from the SARIF tool driver for provenance.
    #[arg(long = "sarif", value_name = "FILE")]
    pub sarif: Vec<PathBuf>,

    /// Snyk agent-scan JSON file (a `ScanPathResult` with an `issues` array,
    /// repeatable).
    #[arg(long = "snyk", value_name = "FILE")]
    pub snyk: Vec<PathBuf>,

    /// Generic scanner JSON file, a top-level array or an object with a
    /// `findings` array (repeatable). The escape hatch for a scanner that
    /// speaks neither SARIF nor the Snyk shape.
    #[arg(long = "supplement", value_name = "FILE")]
    pub supplement: Vec<PathBuf>,

    /// Optional `tools/list`-style snapshot to also scan with the bundled
    /// deterministic lanes, so imports dedup against findings from the same run.
    #[arg(long, value_name = "FILE")]
    pub snapshot: Option<PathBuf>,

    /// Mark every imported finding advisory, so none of them gates (ADR 0041).
    /// Set this for a scanner whose verdicts come from an LLM judge.
    #[arg(long)]
    pub advisory: bool,

    /// Output format.
    #[arg(long, value_enum, default_value_t = SecurityFormat::Pretty)]
    pub format: SecurityFormat,

    /// Exit non-zero when any counted finding is at or above this severity.
    #[arg(long, value_enum, default_value_t = SecurityFailOn::High)]
    pub fail_on: SecurityFailOn,
}

/// How the security findings are rendered.
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum SecurityFormat {
    /// Human-readable summary.
    Pretty,
    /// Machine-readable JSON of the report.
    Json,
    /// SARIF 2.1.0 for code scanning.
    Sarif,
    /// Self-contained HTML vulnerability report grouped by severity, with an
    /// OWASP LLM Top 10 coverage section.
    Html,
    /// GitHub-flavored Markdown vulnerability report (same content as HTML).
    Md,
}

/// Severity floor for the `--fail-on` gate. Mirrors
/// [`SecuritySeverity`] so the CLI can derive `ValueEnum` without putting a
/// clap dependency on the core crate.
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum SecurityFailOn {
    /// Fail on any finding.
    Info,
    /// Fail on low and above.
    Low,
    /// Fail on medium and above.
    Medium,
    /// Fail on high and above (the default).
    High,
    /// Fail only on critical findings.
    Critical,
}

impl SecurityFailOn {
    /// Map the CLI floor onto the core severity.
    pub fn to_severity(self) -> SecuritySeverity {
        match self {
            SecurityFailOn::Info => SecuritySeverity::Info,
            SecurityFailOn::Low => SecuritySeverity::Low,
            SecurityFailOn::Medium => SecuritySeverity::Medium,
            SecurityFailOn::High => SecuritySeverity::High,
            SecurityFailOn::Critical => SecuritySeverity::Critical,
        }
    }
}
