//! Source-aware MCP vulnerability proofing (WOR-1430).
//!
//! mcptest already ingests third-party scanner output. This module adds native
//! source-to-lead analysis: it resolves MCP tool handlers in TypeScript,
//! Python, and Rust source, maps each to risky sink categories (shell, eval,
//! filesystem, network, credentials), and emits advisory findings with file and
//! line locations.
//!
//! Two hard rules, both grounded in honesty about what static analysis can
//! claim. First, every static finding is advisory: a sink near a handler is a
//! lead, never a proof; only a dynamic run against a disposable target can set
//! `confirmed`. Second, the path to that confirmation, the generated PoC suite,
//! is gated behind explicit user consent and ships with sandbox guidance.

mod model;
mod render;
mod scan;

pub use model::{Language, ProofFinding, ProofReport, SinkCategory, PROOF_SCHEMA_VERSION};
pub use render::{to_poc_suite, to_sarif};
pub use scan::{build_report, scan_source};

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

    const TS: &str = r#"
import { Server } from "@modelcontextprotocol/sdk";
const server = new Server();
server.tool("run_command", async (args) => {
  const { execSync } = require("child_process");
  return execSync(args.cmd);
});
server.tool("read_note", async (args) => {
  return fs.readFile(args.path);
});
"#;

    const PY: &str = r#"
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("demo")

@mcp.tool()
def fetch_url(url: str):
    import requests
    return requests.get(url).text
"#;

    const RS: &str = r#"
#[tool(name = "shell")]
fn shell(cmd: String) -> String {
    let out = std::process::Command::new("sh").arg("-c").arg(cmd).output();
    String::new()
}
"#;

    #[test]
    fn maps_typescript_handlers_to_shell_and_fs_sinks() {
        let (findings, handlers) = scan_source("server.ts", Language::TypeScript, TS);
        assert!(handlers.contains(&"run_command".to_string()));
        let shell = findings
            .iter()
            .find(|f| f.category == SinkCategory::Shell)
            .expect("shell sink found");
        assert_eq!(shell.tool.as_deref(), Some("run_command"));
        assert_eq!(shell.severity, SecuritySeverity::High);
        assert!(shell.advisory && !shell.confirmed);
    }

    #[test]
    fn maps_python_fastmcp_handler_to_network_sink() {
        let (findings, handlers) = scan_source("srv.py", Language::Python, PY);
        assert!(handlers.contains(&"fetch_url".to_string()));
        let net = findings
            .iter()
            .find(|f| f.category == SinkCategory::NetworkEgress)
            .expect("network sink found");
        assert_eq!(net.tool.as_deref(), Some("fetch_url"));
    }

    #[test]
    fn maps_rust_tool_attribute_to_process_sink() {
        let (findings, handlers) = scan_source("lib.rs", Language::Rust, RS);
        assert!(handlers.contains(&"shell".to_string()));
        assert!(findings
            .iter()
            .any(|f| f.category == SinkCategory::Shell && f.tool.as_deref() == Some("shell")));
    }

    #[test]
    fn sarif_carries_source_locations_and_advisory_flag() {
        let (findings, handlers) = scan_source("server.ts", Language::TypeScript, TS);
        let report = build_report("src", findings, handlers);
        let sarif = to_sarif(&report);
        let first = &sarif["runs"][0]["results"][0];
        assert!(first["locations"][0]["physicalLocation"]["region"]["startLine"].is_number());
        assert_eq!(first["properties"]["advisory"], true);
        assert_eq!(first["properties"]["confirmed"], false);
    }

    #[test]
    fn poc_suite_is_a_negative_path_probe_with_consent_header() {
        let (findings, handlers) = scan_source("server.ts", Language::TypeScript, TS);
        let report = build_report("src", findings, handlers);
        let suite = to_poc_suite(&report, "node ./server.js");
        assert!(suite.contains("disposable, sandboxed copy"));
        assert!(
            suite.contains("negative_path:"),
            "probes input validation, not exploitation"
        );
        assert!(suite.contains("run_command"));
        // The suite must validate as a mcptest suite (schema header present).
        assert!(suite.contains("https://mcptest.sh/schema/v1.json"));
    }

    #[test]
    fn fails_at_threshold_reflects_findings() {
        let (findings, handlers) = scan_source("server.ts", Language::TypeScript, TS);
        let report = build_report("src", findings, handlers);
        assert!(report.fails_at(SecuritySeverity::High));
        assert!(!report.fails_at(SecuritySeverity::Critical));
    }

    // -- WOR-1587: intra-function taint heuristic ----------------------------

    /// A handler whose shell sink runs a constant command: the parameter never
    /// reaches the sink, so the finding is an untainted proximity lead.
    const TS_UNTAINTED: &str = r#"
server.tool("status", async (args) => {
  const { execSync } = require("child_process");
  return execSync("uptime");
});
"#;

    /// A handler that passes its parameter to the sink through a local variable
    /// (a one-hop dataflow), which the heuristic must follow.
    const TS_HOP: &str = r#"
server.tool("run", async (args) => {
  const cmd = args.command;
  return execSync(cmd);
});
"#;

    #[test]
    fn a_parameter_reaching_a_sink_is_a_tainted_high_confidence_flow() {
        let (findings, _) = scan_source("server.ts", Language::TypeScript, TS);
        let flow = findings
            .iter()
            .find(|f| {
                f.category == SinkCategory::Shell
                    && f.tool.as_deref() == Some("run_command")
                    && f.snippet.contains("execSync(args.cmd)")
            })
            .expect("execSync(args.cmd) sink found");
        assert!(flow.tainted, "args reaches execSync");
        assert_eq!(flow.confidence, "high");
    }

    #[test]
    fn a_constant_sink_in_a_handler_is_downgraded_to_an_untainted_lead() {
        let (findings, _) = scan_source("server.ts", Language::TypeScript, TS_UNTAINTED);
        let lead = findings
            .iter()
            .find(|f| f.category == SinkCategory::Shell && f.snippet.contains("uptime"))
            .expect("execSync(\"uptime\") sink found");
        assert!(!lead.tainted, "the constant command is not tainted by args");
        assert_eq!(lead.confidence, "low");
        // The tainted flow outranks the untainted lead, the acceptance property.
        let (tainted_findings, _) = scan_source("server.ts", Language::TypeScript, TS);
        let tainted = tainted_findings
            .iter()
            .find(|f| f.snippet.contains("execSync(args.cmd)"))
            .unwrap();
        assert!(rank(&tainted.confidence) > rank(&lead.confidence));
    }

    #[test]
    fn taint_follows_a_one_hop_local_assignment() {
        let (findings, _) = scan_source("server.ts", Language::TypeScript, TS_HOP);
        let flow = findings
            .iter()
            .find(|f| f.category == SinkCategory::Shell && f.snippet.contains("execSync(cmd)"))
            .expect("execSync(cmd) sink found");
        assert!(
            flow.tainted,
            "args.command flows into cmd then into execSync"
        );
        assert_eq!(flow.confidence, "high");
    }

    #[test]
    fn python_parameter_to_network_sink_is_tainted() {
        let (findings, _) = scan_source("srv.py", Language::Python, PY);
        let net = findings
            .iter()
            .find(|f| f.category == SinkCategory::NetworkEgress)
            .expect("network sink found");
        assert!(net.tainted, "url reaches requests.get");
        assert_eq!(net.confidence, "high");
    }

    #[test]
    fn rust_fn_parameter_to_process_sink_is_tainted() {
        let (findings, _) = scan_source("lib.rs", Language::Rust, RS);
        let shell = findings
            .iter()
            .find(|f| f.category == SinkCategory::Shell)
            .expect("shell sink found");
        assert!(shell.tainted, "cmd reaches Command");
        assert_eq!(shell.confidence, "high");
    }

    /// Confidence ordering for the acceptance comparison.
    fn rank(confidence: &str) -> u8 {
        match confidence {
            "high" => 3,
            "medium" => 2,
            _ => 1,
        }
    }
}
