//! Findings model for source-aware vulnerability proofing (WOR-1430).
//!
//! A finding maps an advertised MCP tool handler in source to a risky sink (a
//! shell exec, a filesystem write, a network call, a credential read, a dynamic
//! eval). Every static finding is advisory by construction: a sink near a
//! handler is a lead, not a proof. The honest framing matters, so the model
//! carries an explicit `advisory` flag and a `confirmed` flag that only a
//! dynamic run against a disposable target can set.

use serde::{Deserialize, Serialize};

use crate::security::SecuritySeverity;

/// The category of risky sink a finding points at.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum SinkCategory {
    /// Shell or subprocess execution.
    Shell,
    /// Filesystem write.
    FilesystemWrite,
    /// Filesystem read (potential path traversal).
    FilesystemRead,
    /// Outbound network call (potential SSRF).
    NetworkEgress,
    /// Credential or secret access.
    CredentialAccess,
    /// Dynamic evaluation or dynamic import.
    DynamicEval,
}

impl SinkCategory {
    /// The SARIF/finding rule id for this category.
    pub fn rule_id(self) -> &'static str {
        match self {
            SinkCategory::Shell => "SRCPROOF-SHELL",
            SinkCategory::FilesystemWrite => "SRCPROOF-FS-WRITE",
            SinkCategory::FilesystemRead => "SRCPROOF-FS-READ",
            SinkCategory::NetworkEgress => "SRCPROOF-NET",
            SinkCategory::CredentialAccess => "SRCPROOF-CRED",
            SinkCategory::DynamicEval => "SRCPROOF-EVAL",
        }
    }

    /// The default severity for an unconfirmed finding of this category.
    pub fn severity(self) -> SecuritySeverity {
        match self {
            SinkCategory::Shell | SinkCategory::DynamicEval => SecuritySeverity::High,
            SinkCategory::FilesystemWrite
            | SinkCategory::NetworkEgress
            | SinkCategory::CredentialAccess => SecuritySeverity::Medium,
            SinkCategory::FilesystemRead => SecuritySeverity::Low,
        }
    }

    /// A short, human-readable label.
    pub fn label(self) -> &'static str {
        match self {
            SinkCategory::Shell => "shell/process execution",
            SinkCategory::FilesystemWrite => "filesystem write",
            SinkCategory::FilesystemRead => "filesystem read",
            SinkCategory::NetworkEgress => "network egress",
            SinkCategory::CredentialAccess => "credential access",
            SinkCategory::DynamicEval => "dynamic eval/import",
        }
    }

    /// A remediation pointer for the finding.
    pub fn remediation(self) -> &'static str {
        match self {
            SinkCategory::Shell => {
                "never pass tool arguments into a shell; use an argv array and an allowlist"
            }
            SinkCategory::FilesystemWrite => {
                "confine writes to a sandbox root and reject path traversal"
            }
            SinkCategory::FilesystemRead => "canonicalize and bound read paths to an allowed root",
            SinkCategory::NetworkEgress => "validate and allowlist outbound hosts to prevent SSRF",
            SinkCategory::CredentialAccess => {
                "do not surface secrets through tool output; scope env access"
            }
            SinkCategory::DynamicEval => "remove dynamic eval of tool input; parse explicitly",
        }
    }
}

/// One source language the scanner recognizes.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Language {
    /// TypeScript or JavaScript.
    TypeScript,
    /// Python.
    Python,
    /// Rust.
    Rust,
}

impl Language {
    /// Resolve a language from a file extension, or `None` if unrecognized.
    pub fn from_extension(ext: &str) -> Option<Self> {
        match ext.to_ascii_lowercase().as_str() {
            "ts" | "tsx" | "js" | "mjs" | "cjs" => Some(Language::TypeScript),
            "py" => Some(Language::Python),
            "rs" => Some(Language::Rust),
            _ => None,
        }
    }

    /// A stable label.
    pub fn label(self) -> &'static str {
        match self {
            Language::TypeScript => "typescript",
            Language::Python => "python",
            Language::Rust => "rust",
        }
    }
}

/// One source-aware finding.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProofFinding {
    /// The advertised tool the handler implements, when one was resolved.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tool: Option<String>,
    /// The risky sink category.
    pub category: SinkCategory,
    /// Severity (the category default until a dynamic run confirms it).
    pub severity: SecuritySeverity,
    /// The source language.
    pub language: Language,
    /// The file the finding is in.
    pub file: String,
    /// 1-based line number of the sink.
    pub line: usize,
    /// The matched source line, trimmed.
    pub snippet: String,
    /// Heuristic confidence (`low`, `medium`, `high`). A tainted flow (an
    /// untrusted handler parameter reaches the sink) reads `high`; a sink in a
    /// handler body that no parameter reaches is downgraded to `low`; a sink
    /// with no resolved handler keeps the category default.
    pub confidence: String,
    /// True when an intra-function dataflow heuristic found a handler parameter
    /// (a taint source) reaching this sink within the same handler body, either
    /// directly or through a local assignment. `false` is the conservative
    /// default: a proximity lead, not a proven flow.
    pub tainted: bool,
    /// Always true for static findings: a sink near a handler is a lead.
    pub advisory: bool,
    /// Set only when a dynamic run against a disposable target proved it.
    pub confirmed: bool,
}

/// A full source-proof report over a scanned tree.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProofReport {
    /// Schema id.
    pub schema_version: String,
    /// The root path scanned.
    pub root: String,
    /// Tool handlers resolved, by name.
    pub handlers: Vec<String>,
    /// The advisory findings, sorted for stable output.
    pub findings: Vec<ProofFinding>,
}

/// Schema id stamped on every source-proof report.
pub const PROOF_SCHEMA_VERSION: &str = "mcptest.sh/source-proof/v1";

impl ProofReport {
    /// Whether any finding sits at or above a severity (for `--fail-on`).
    pub fn fails_at(&self, threshold: SecuritySeverity) -> bool {
        self.findings.iter().any(|f| f.severity >= threshold)
    }
}
