//! OWASP Agentic AI threat-taxonomy coverage mapping for the security catalog.
//!
//! Companion to [`super::owasp`] (the LLM Top 10). The OWASP Agentic Security
//! Initiative's "Agentic AI: Threats and Mitigations" guide publishes a
//! separate, agent-centric taxonomy (T1 through T15). mcptest's checks are
//! already tagged with OWASP LLM ids, so rather than re-tag every check this
//! module derives agentic coverage through a fixed LLM-to-agentic crosswalk and
//! reads the references the checks already carry. Threats with no covering rule
//! are reported as gaps: the multi-agent and human-facing threats (T12 to T15)
//! are out of scope for single-server black-box testing and stay uncovered by
//! design.

use super::owasp::owasp_ids;
use super::SecurityCheck;

/// One agentic threat from the OWASP ASI taxonomy, with a remediation pointer
/// the report attaches to findings mapped to it.
pub struct AgenticThreat {
    /// Identifier as used in this module, for example `T1`.
    pub id: &'static str,
    /// Human-readable title from the OWASP ASI taxonomy.
    pub title: &'static str,
    /// One-line remediation guidance for findings mapped to this threat.
    pub remediation: &'static str,
}

/// The OWASP Agentic AI threat taxonomy (T1 through T15), ordered by id so the
/// report renders a stable table. Ids and titles follow the OWASP ASI "Agentic
/// AI: Threats and Mitigations" guide.
pub const OWASP_AGENTIC_THREATS: &[AgenticThreat] = &[
    AgenticThreat {
        id: "T1",
        title: "Memory Poisoning",
        remediation: "Validate and isolate memory/context an agent persists or retrieves; treat server-supplied content as untrusted.",
    },
    AgenticThreat {
        id: "T2",
        title: "Tool Misuse",
        remediation: "Constrain tool capability and validate tool descriptions; deceptive descriptions steer an agent into abusing a tool.",
    },
    AgenticThreat {
        id: "T3",
        title: "Privilege Compromise",
        remediation: "Scope tools to least privilege; flag destructive or high-capability tools and privilege-escalating chains.",
    },
    AgenticThreat {
        id: "T4",
        title: "Resource Overload",
        remediation: "Bound tool cost, output size, and call depth to resist compute/memory exhaustion.",
    },
    AgenticThreat {
        id: "T5",
        title: "Cascading Hallucination Attacks",
        remediation: "Ground answers in verifiable context and gate on factuality so false output does not propagate through later steps.",
    },
    AgenticThreat {
        id: "T6",
        title: "Intent Breaking & Goal Manipulation",
        remediation: "Treat tool/prompt/resource text as untrusted; a smuggled instruction can redirect the agent's goal.",
    },
    AgenticThreat {
        id: "T7",
        title: "Misaligned & Deceptive Behaviors",
        remediation: "Detect tool definitions or outputs that steer the agent toward goals the user did not ask for.",
    },
    AgenticThreat {
        id: "T8",
        title: "Repudiation & Untraceability",
        remediation: "Emit a tamper-evident record of tool calls and decisions so agent actions can be attributed and audited.",
    },
    AgenticThreat {
        id: "T9",
        title: "Identity Spoofing & Impersonation",
        remediation: "Pin server identity and detect tool shadowing/squatting so one server cannot impersonate another's tools.",
    },
    AgenticThreat {
        id: "T10",
        title: "Overwhelming Human-in-the-Loop",
        remediation: "Bound the rate and volume of approval prompts; an attacker can flood a human reviewer into rubber-stamping.",
    },
    AgenticThreat {
        id: "T11",
        title: "Unexpected RCE and Code Attacks",
        remediation: "Validate and encode tool output before it reaches a shell, query, or eval; never act on it unchecked.",
    },
    AgenticThreat {
        id: "T12",
        title: "Agent Communication Poisoning",
        remediation: "Authenticate and validate inter-agent messages (multi-agent; out of scope for single-server testing).",
    },
    AgenticThreat {
        id: "T13",
        title: "Rogue Agents in Multi-Agent Systems",
        remediation: "Verify provenance of participating agents/servers and detect rug-pulls against an approved baseline.",
    },
    AgenticThreat {
        id: "T14",
        title: "Human Attacks on Multi-Agent Systems",
        remediation: "Harden trust boundaries between agents (multi-agent; out of scope for single-server testing).",
    },
    AgenticThreat {
        id: "T15",
        title: "Human Manipulation",
        remediation: "Constrain how agent output can persuade the user (social-engineering surface; out of scope for protocol testing).",
    },
];

/// Look up an agentic threat by bare id (for example `"T1"`).
pub fn threat(id: &str) -> Option<&'static AgenticThreat> {
    OWASP_AGENTIC_THREATS.iter().find(|t| t.id == id)
}

/// Fixed crosswalk from an OWASP LLM Top 10 id to the agentic threats it maps
/// onto. Documented inline so the rationale is auditable; an empty slice means
/// the LLM category has no clean single-server agentic analog.
fn agentic_ids_for_llm(llm_id: &str) -> &'static [&'static str] {
    match llm_id {
        // Prompt injection redirects the goal and abuses tools.
        "LLM01" => &["T2", "T6"],
        // Sensitive-information disclosure has no clean agentic-threat analog.
        "LLM02" => &[],
        // Supply chain: rug-pulls/shadowing surface as impersonation + rogue agents.
        "LLM03" => &["T9", "T13"],
        // Data/model poisoning is memory poisoning.
        "LLM04" => &["T1"],
        // Improper output handling is the RCE/code-attack path.
        "LLM05" => &["T11"],
        // Excessive agency is tool misuse + privilege compromise.
        "LLM06" => &["T2", "T3"],
        // System-prompt leakage enables goal manipulation.
        "LLM07" => &["T6"],
        // Vector/embedding weaknesses poison retrieved memory.
        "LLM08" => &["T1"],
        // Misinformation is cascading hallucination + deceptive behavior.
        "LLM09" => &["T5", "T7"],
        // Unbounded consumption is resource overload + flooding the human.
        "LLM10" => &["T4", "T10"],
        _ => &[],
    }
}

/// Translate a check's references into the agentic threat ids it maps onto, via
/// its OWASP LLM ids. Order is stable (reference order, deduped).
pub fn agentic_ids(references: &[&str]) -> Vec<String> {
    let mut out: Vec<String> = Vec::new();
    for llm in owasp_ids(references) {
        for t in agentic_ids_for_llm(&llm) {
            let id = (*t).to_string();
            if threat(&id).is_some() && !out.contains(&id) {
                out.push(id);
            }
        }
    }
    out
}

/// One row of the agentic coverage view: a threat and the `SEC-NNN` rule ids in
/// the catalog that map onto it.
pub struct AgenticCoverage {
    /// The threat this row covers.
    pub threat: &'static AgenticThreat,
    /// Rule ids (for example `SEC-001`) that map onto this threat.
    pub rule_ids: Vec<String>,
}

impl AgenticCoverage {
    /// True when at least one catalog rule maps onto this threat.
    pub fn is_covered(&self) -> bool {
        !self.rule_ids.is_empty()
    }
}

/// Build the agentic coverage view for a check catalog. One row per threat, in
/// declaration order, with covering rule ids sorted for stable output.
pub fn agentic_coverage(checks: &[Box<dyn SecurityCheck>]) -> Vec<AgenticCoverage> {
    OWASP_AGENTIC_THREATS
        .iter()
        .map(|t| {
            let mut rule_ids: Vec<String> = checks
                .iter()
                .filter(|c| agentic_ids(c.references()).iter().any(|id| id == t.id))
                .map(|c| c.id().to_string())
                .collect();
            rule_ids.sort();
            rule_ids.dedup();
            AgenticCoverage {
                threat: t,
                rule_ids,
            }
        })
        .collect()
}

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

    #[test]
    fn taxonomy_has_fifteen_threats_with_unique_ids() {
        assert_eq!(OWASP_AGENTIC_THREATS.len(), 15);
        let mut ids: Vec<&str> = OWASP_AGENTIC_THREATS.iter().map(|t| t.id).collect();
        ids.sort();
        ids.dedup();
        assert_eq!(ids.len(), 15);
    }

    #[test]
    fn crosswalk_only_references_real_threat_ids() {
        // No dangling ids: every crosswalk target must exist in the taxonomy.
        for llm in [
            "LLM01", "LLM02", "LLM03", "LLM04", "LLM05", "LLM06", "LLM07", "LLM08", "LLM09",
            "LLM10",
        ] {
            for t in agentic_ids_for_llm(llm) {
                assert!(threat(t).is_some(), "crosswalk {llm} -> {t} is dangling");
            }
        }
    }

    #[test]
    fn agentic_ids_translate_llm_references() {
        // LLM06 (Excessive Agency) maps to Tool Misuse + Privilege Compromise.
        let ids = agentic_ids(&["OWASP LLM06", "CWE-862"]);
        assert!(ids.contains(&"T2".to_string()));
        assert!(ids.contains(&"T3".to_string()));
    }

    #[test]
    fn coverage_has_one_row_per_threat() {
        assert_eq!(
            agentic_coverage(&surface_checks()).len(),
            OWASP_AGENTIC_THREATS.len()
        );
    }

    #[test]
    fn multi_agent_threats_are_uncovered_gaps() {
        // Single-server testing cannot reach the multi-agent threats; they must
        // surface as gaps, not be force-mapped.
        let cov = agentic_coverage(&surface_checks());
        for id in ["T12", "T14", "T15"] {
            let row = cov.iter().find(|r| r.threat.id == id).unwrap();
            assert!(!row.is_covered(), "{id} should be an uncovered gap");
        }
    }

    #[test]
    fn tool_misuse_is_covered_by_the_surface_lane() {
        let cov = agentic_coverage(&surface_checks());
        let t2 = cov.iter().find(|r| r.threat.id == "T2").unwrap();
        assert!(t2.is_covered(), "T2 Tool Misuse should be covered");
    }
}
