//! Description mutations: deterministic edits that degrade a single tool's
//! description so the benchmark can measure the selection impact.
//!
//! Each mutation targets a tool by name and rewrites its `description`. The
//! benchmark applies one mutation at a time against the same catalog and
//! re-scores selection, so the delta is attributable to that one edit. The
//! edits are intentionally crude and reproducible (no model, no randomness):
//! they model the description smells a real catalog drifts into.

use serde::{Deserialize, Serialize};
use serde_json::Value;

/// One degradation applied to a tool's description.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Mutation {
    /// Remove the description entirely (sets it to an empty string).
    Blank,
    /// Keep only the first `chars` characters of the description.
    Truncate {
        /// Number of leading characters to keep.
        chars: usize,
    },
    /// Remove every case-insensitive occurrence of a content word. Models the
    /// loss of the keyword that made the description match an intent.
    DropKeyword {
        /// The word to strip.
        word: String,
    },
    /// Replace the description with a generic, contentless phrase.
    Genericize,
}

impl Mutation {
    /// A short stable label for reports.
    pub fn label(&self) -> String {
        match self {
            Mutation::Blank => "blank".into(),
            Mutation::Truncate { chars } => format!("truncate({chars})"),
            Mutation::DropKeyword { word } => format!("drop_keyword({word})"),
            Mutation::Genericize => "genericize".into(),
        }
    }

    /// Apply the mutation to a description string.
    fn rewrite(&self, description: &str) -> String {
        match self {
            Mutation::Blank => String::new(),
            Mutation::Truncate { chars } => description.chars().take(*chars).collect(),
            Mutation::DropKeyword { word } => drop_word(description, word),
            Mutation::Genericize => "This tool performs an operation.".into(),
        }
    }
}

/// Remove whole-word case-insensitive occurrences of `word`, collapsing the
/// double spaces the removal leaves behind.
fn drop_word(text: &str, word: &str) -> String {
    let kept: Vec<&str> = text
        .split_whitespace()
        .filter(|token| {
            let bare: String = token
                .chars()
                .filter(|c| c.is_ascii_alphanumeric())
                .collect();
            !bare.eq_ignore_ascii_case(word)
        })
        .collect();
    kept.join(" ")
}

/// Return a copy of `catalog` with the named tool's description mutated.
///
/// When no tool matches `tool_name`, the catalog is returned unchanged so a
/// stale benchmark entry degrades gracefully rather than panicking.
pub fn apply(catalog: &Value, tool_name: &str, mutation: &Mutation) -> Value {
    let mut next = catalog.clone();
    if let Some(tools) = next.get_mut("tools").and_then(Value::as_array_mut) {
        for tool in tools {
            if tool.get("name").and_then(Value::as_str) == Some(tool_name) {
                let current = tool
                    .get("description")
                    .and_then(Value::as_str)
                    .unwrap_or("");
                let rewritten = mutation.rewrite(current);
                if let Some(obj) = tool.as_object_mut() {
                    obj.insert("description".into(), Value::String(rewritten));
                }
            }
        }
    }
    next
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    fn catalog() -> Value {
        json!({
            "tools": [{
                "name": "get_weather",
                "description": "Return the current weather forecast for a city."
            }]
        })
    }

    fn desc(catalog: &Value) -> String {
        catalog["tools"][0]["description"]
            .as_str()
            .unwrap()
            .to_string()
    }

    #[test]
    fn blank_clears_the_description() {
        let out = apply(&catalog(), "get_weather", &Mutation::Blank);
        assert_eq!(desc(&out), "");
    }

    #[test]
    fn truncate_keeps_the_prefix() {
        let out = apply(&catalog(), "get_weather", &Mutation::Truncate { chars: 6 });
        assert_eq!(desc(&out), "Return");
    }

    #[test]
    fn drop_keyword_removes_the_word_case_insensitively() {
        let out = apply(
            &catalog(),
            "get_weather",
            &Mutation::DropKeyword {
                word: "weather".into(),
            },
        );
        assert!(
            !desc(&out).to_lowercase().contains("weather"),
            "{}",
            desc(&out)
        );
        assert!(desc(&out).contains("forecast"));
    }

    #[test]
    fn genericize_replaces_the_whole_description() {
        let out = apply(&catalog(), "get_weather", &Mutation::Genericize);
        assert_eq!(desc(&out), "This tool performs an operation.");
    }

    #[test]
    fn an_unknown_tool_leaves_the_catalog_unchanged() {
        let out = apply(&catalog(), "missing", &Mutation::Blank);
        assert_eq!(out, catalog());
    }
}
