//! Render a source-proof report: a SARIF document with source locations, and a
//! reproducible mcptest PoC suite that turns a static lead into a runnable
//! negative-path test against a disposable target.

use serde_json::{json, Value};

use super::model::ProofReport;

/// Render the report as a SARIF 2.1.0 document with file/line locations.
///
/// Every result is `level: warning` and tagged advisory in its message: a
/// static lead is not a proven vulnerability. A consumer that ingests this into
/// `mcptest security import` sees the source locations.
pub fn to_sarif(report: &ProofReport) -> Value {
    let results: Vec<Value> = report
        .findings
        .iter()
        .map(|f| {
            json!({
                "ruleId": f.category.rule_id(),
                "level": "warning",
                "message": {
                    "text": format!(
                        "Advisory: tool `{}` reaches a {} sink. {}",
                        f.tool.as_deref().unwrap_or("<unmapped>"),
                        f.category.label(),
                        f.category.remediation()
                    )
                },
                "locations": [{
                    "physicalLocation": {
                        "artifactLocation": { "uri": f.file },
                        "region": { "startLine": f.line, "snippet": { "text": f.snippet } }
                    }
                }],
                "properties": {
                    "advisory": f.advisory,
                    "confirmed": f.confirmed,
                    "confidence": f.confidence,
                    "tainted": f.tainted,
                    "tool": f.tool
                }
            })
        })
        .collect();

    json!({
        "version": "2.1.0",
        "$schema": "https://json.schemastore.org/sarif-2.1.0.json",
        "runs": [{
            "tool": { "driver": {
                "name": "mcptest-source-proof",
                "informationUri": "https://mcptest.sh",
                "rules": []
            }},
            "results": results
        }]
    })
}

/// Render a reproducible mcptest PoC suite from the findings.
///
/// The suite is a negative-path security probe: for each tool that reaches a
/// risky sink, it calls the tool with a benign boundary-pushing argument and
/// asserts the server fails cleanly (a well-formed error, no partial state)
/// rather than executing the dangerous operation. This is the path to dynamic
/// confirmation: a developer points `server.command` at a disposable target and
/// runs it. The header carries the consent and sandbox guidance.
pub fn to_poc_suite(report: &ProofReport, target_command: &str) -> String {
    let mut out = String::new();
    out.push_str("# yaml-language-server: $schema=https://mcptest.sh/schema/v1.json\n#\n");
    out.push_str("# Source-proof PoC suite generated by `mcptest security proof` (advisory).\n");
    out.push_str("# These tests turn static leads into negative-path probes. Run them ONLY\n");
    out.push_str("# against a disposable, sandboxed copy of the server you are authorized to\n");
    out.push_str("# test. Each probe sends a benign boundary input and asserts a clean error;\n");
    out.push_str("# it does not execute the dangerous operation. Review before running.\n#\n");
    out.push_str(&format!("# root: {}\n", report.root));
    out.push_str(&format!("# findings: {}\n\n", report.findings.len()));

    out.push_str("servers:\n");
    out.push_str("  target:\n");
    out.push_str(&format!(
        "    command: [{}]\n\n",
        quote_command(target_command)
    ));
    out.push_str("tools:\n");

    // One negative-path probe per (tool, category), deduped, in stable order.
    // The negative_path block sends taxonomy-keyed bad inputs and gates on the
    // server rejecting each cleanly: a tool that reaches a dangerous sink must
    // validate input before it gets there. This is a probe, not an exploit.
    let mut seen: Vec<(String, &str)> = Vec::new();
    for f in &report.findings {
        let Some(tool) = &f.tool else { continue };
        let key = (tool.clone(), f.category.rule_id());
        if seen.contains(&key) {
            continue;
        }
        seen.push(key);
        out.push_str(&format!(
            "  - name: {} validates input before reaching a {} sink\n",
            tool,
            f.category.label()
        ));
        out.push_str("    server: target\n");
        out.push_str(&format!("    tool: {tool}\n"));
        out.push_str("    args: {}\n");
        out.push_str("    negative_path:\n");
        out.push_str("      checks: [missing_required, wrong_type, oversized]\n");
    }
    if seen.is_empty() {
        out.push_str("  []\n");
    }
    out
}

/// Quote a target command into the YAML inline array body.
fn quote_command(command: &str) -> String {
    command
        .split_whitespace()
        .map(|part| format!("\"{part}\""))
        .collect::<Vec<_>>()
        .join(", ")
}
