//! Behavior tests for the deterministic TDQS scoring math.

use super::*;
use serde_json::json;

/// A well-documented tool: long-enough description that names a return shape, a
/// fully typed and described parameter, and annotations.
fn clean_tool(name: &str) -> Value {
    json!({
        "name": name,
        "description": "Creates an order and returns the new order id and status.",
        "inputSchema": {
            "type": "object",
            "properties": {
                "customer_id": { "type": "string", "description": "the paying customer id" }
            }
        },
        "annotations": { "destructiveHint": false }
    })
}

#[test]
fn clean_tool_scores_full_marks() {
    let tool = clean_tool("create_order");
    let scored = score_tool(&tool);
    assert_eq!(scored.signals.description_present, 1.0);
    assert_eq!(scored.signals.params_documented, 1.0);
    assert_eq!(scored.signals.parameter_semantics, 1.0);
    assert_eq!(scored.signals.conciseness, 1.0);
    assert_eq!(scored.signals.return_format, 1.0);
    assert_eq!(scored.signals.annotations_present, 1.0);
    assert_eq!(scored.score, 1.0);
}

#[test]
fn undocumented_parameter_drags_the_param_signals() {
    let tool = json!({
        "name": "create_order",
        "description": "Creates an order and returns the new order id.",
        "inputSchema": {
            "type": "object",
            "properties": {
                "customer_id": { "type": "string", "description": "the customer id" },
                "amount": { "type": "number" }
            }
        },
        "annotations": {}
    });
    let scored = score_tool(&tool);
    // One of two parameters is documented.
    assert_eq!(scored.signals.params_documented, 0.5);
    // One of two parameters has both type and description.
    assert_eq!(scored.signals.parameter_semantics, 0.5);
    assert!(scored.score < 1.0);
}

#[test]
fn empty_description_zeroes_presence_conciseness_and_return_format() {
    let tool = json!({ "name": "x", "description": "", "annotations": {} });
    let scored = score_tool(&tool);
    assert_eq!(scored.signals.description_present, 0.0);
    assert_eq!(scored.signals.conciseness, 0.0);
    assert_eq!(scored.signals.return_format, 0.0);
}

#[test]
fn parameterless_tool_scores_full_on_both_param_signals() {
    let tool = json!({
        "name": "ping",
        "description": "Pings the server and returns ok when it is reachable.",
        "inputSchema": { "type": "object", "properties": {} },
        "annotations": {}
    });
    let scored = score_tool(&tool);
    assert_eq!(scored.signals.params_documented, 1.0);
    assert_eq!(scored.signals.parameter_semantics, 1.0);
}

#[test]
fn conciseness_decays_for_a_runaway_description() {
    // 121 words is one word past twice the 60-word budget, so the score floors.
    let long = "word ".repeat(121);
    let tool = json!({ "name": "verbose", "description": long });
    assert_eq!(conciseness_score(&tool), 0.0);

    // Just over the budget decays but does not floor.
    let medium = "word ".repeat(90);
    let tool = json!({ "name": "medium", "description": medium });
    let score = conciseness_score(&tool);
    assert!(score > 0.0 && score < 1.0);
}

#[test]
fn aggregate_weights_mean_sixty_and_min_forty() {
    // Two tools, scores 1.0 and 0.5: mean 0.75, min 0.5.
    // Expected aggregate = 0.6 * 0.75 + 0.4 * 0.5 = 0.45 + 0.20 = 0.65.
    let scored = vec![
        ToolScore {
            name: "a".into(),
            signals: ToolSignals {
                description_present: 1.0,
                params_documented: 1.0,
                parameter_semantics: 1.0,
                conciseness: 1.0,
                return_format: 1.0,
                annotations_present: 1.0,
            },
            score: 1.0,
        },
        ToolScore {
            name: "b".into(),
            signals: ToolSignals {
                description_present: 0.5,
                params_documented: 0.5,
                parameter_semantics: 0.5,
                conciseness: 0.5,
                return_format: 0.5,
                annotations_present: 0.5,
            },
            score: 0.5,
        },
    ];
    let aggregate = aggregate_quality(&scored).expect("two tools aggregate");
    assert!((aggregate - 0.65).abs() < 1e-9);
}

/// A tool that scores zero on every signal: empty description (presence,
/// conciseness, return-format all zero), an undocumented untyped parameter
/// (params and semantics zero), and no annotations.
fn zero_tool(name: &str) -> Value {
    json!({
        "name": name,
        "description": "",
        "inputSchema": { "type": "object", "properties": { "x": {} } }
    })
}

#[test]
fn one_bad_tool_drags_the_aggregate_below_the_mean() {
    // Nine perfect tools and one zero tool. Mean is 0.9, min is 0.0.
    // Aggregate = 0.6 * 0.9 + 0.4 * 0.0 = 0.54, well below the 0.9 mean.
    let mut tools: Vec<Value> = (0..9).map(|i| clean_tool(&format!("good_{i}"))).collect();
    tools.push(zero_tool("bad"));
    let catalog = json!({ "tools": tools });
    let report = score_catalog(&catalog);
    let mean = report.mean_tool_score().expect("mean");
    let aggregate = report.definition_quality.expect("aggregate");
    assert!((mean - 0.9).abs() < 1e-9);
    assert!(aggregate < mean);
    assert!((aggregate - 0.54).abs() < 1e-9);
}

/// A clean tool with a distinct description, so a catalog of these does not
/// trip the identical-description disambiguation penalty.
fn clean_tool_with(name: &str, description: &str) -> Value {
    let mut tool = clean_tool(name);
    tool["description"] = json!(description);
    tool
}

#[test]
fn clean_catalog_scores_near_perfect() {
    let catalog = json!({
        "tools": [
            clean_tool_with("create_order", "Creates an order and returns its new id."),
            clean_tool_with("list_orders", "Lists open orders and returns an array of them."),
            clean_tool_with("delete_order", "Deletes an order and returns whether it was removed."),
        ]
    });
    let report = score_catalog(&catalog);
    assert_eq!(report.definition_quality, Some(1.0));
    assert_eq!(report.coherence.score(), 1.0);
}

#[test]
fn empty_catalog_has_no_aggregate() {
    let report = score_catalog(&json!({ "tools": [] }));
    assert!(report.tools.is_empty());
    assert_eq!(report.definition_quality, None);
    assert_eq!(report.mean_tool_score(), None);
    assert_eq!(report.min_tool_score(), None);
}

#[test]
fn missing_tools_array_scores_as_empty_catalog() {
    let report = score_catalog(&json!({}));
    assert!(report.tools.is_empty());
    assert_eq!(report.definition_quality, None);
}

#[test]
fn report_round_trips_through_json() {
    let report = score_catalog(&json!({ "tools": [clean_tool("create_order")] }));
    let encoded = serde_json::to_string(&report).expect("serialize");
    let decoded: TdqsReport = serde_json::from_str(&encoded).expect("deserialize");
    assert_eq!(decoded, report);
}

#[test]
fn min_tool_score_reports_the_worst_tool() {
    let catalog = json!({ "tools": [clean_tool("good"), zero_tool("bad")] });
    let report = score_catalog(&catalog);
    let min = report.min_tool_score().expect("min");
    assert_eq!(min, 0.0);
}
