use fp_checker_core::{
    check_code, render_report, CheckerConfig, CustomRuleConfig, CustomRuleMatchType,
    CustomRuleTarget, OutputFormat, RuleId, Severity,
};
use serde_json::Value;
use std::collections::BTreeSet;

#[test]
fn text_report_contains_summary_and_diagnostics() {
    let source = "STATE = []\n\ndef f(items=[]):\n    print(items)\n    return STATE\n";
    let report = check_code(source, "memory.py", &CheckerConfig::default());
    let text = render_report(&report, OutputFormat::Text).unwrap();

    assert!(text.contains("fp-checker report"));
    assert!(text.contains("checked_functions=1"));
    assert!(text.contains("mutable_default_argument"));
    assert!(text.contains("direct_io"));
}

#[test]
fn json_report_matches_expected_shape() {
    let source = "STATE = []\n\ndef f(items=[]):\n    print(items)\n    return STATE\n";
    let report = check_code(source, "memory.py", &CheckerConfig::default());
    let json = render_report(&report, OutputFormat::Json).unwrap();
    let value: Value = serde_json::from_str(&json).unwrap();

    assert!(value.get("summary").and_then(Value::as_object).is_some());
    assert!(value.get("files").and_then(Value::as_array).is_some());

    let summary = value.get("summary").unwrap();
    assert!(summary.get("files").and_then(Value::as_u64).is_some());
    assert!(summary
        .get("checked_functions")
        .and_then(Value::as_u64)
        .is_some());
    assert!(summary
        .get("ignored_functions")
        .and_then(Value::as_u64)
        .is_some());
    assert!(summary.get("diagnostics").and_then(Value::as_u64).is_some());

    let first_file = &value.get("files").and_then(Value::as_array).unwrap()[0];
    assert!(first_file.get("path").and_then(Value::as_str).is_some());
    assert!(first_file
        .get("checked_functions")
        .and_then(Value::as_u64)
        .is_some());
    assert!(first_file
        .get("ignored_functions")
        .and_then(Value::as_u64)
        .is_some());
    assert!(first_file
        .get("diagnostics")
        .and_then(Value::as_array)
        .is_some());

    let first_diagnostic = &first_file
        .get("diagnostics")
        .and_then(Value::as_array)
        .unwrap()[0];
    assert!(first_diagnostic
        .get("rule_id")
        .and_then(Value::as_str)
        .is_some());
    assert!(first_diagnostic
        .get("severity")
        .and_then(Value::as_str)
        .is_some());
    assert!(first_diagnostic
        .get("message")
        .and_then(Value::as_str)
        .is_some());
    assert!(first_diagnostic
        .get("location")
        .and_then(Value::as_object)
        .is_some());
    assert!(first_diagnostic
        .get("location")
        .and_then(|location| location.get("start"))
        .and_then(Value::as_object)
        .is_some());
    assert!(first_diagnostic
        .get("fix_hint")
        .and_then(Value::as_object)
        .is_some());
    assert!(first_diagnostic
        .get("fix_hint")
        .and_then(|fix_hint| fix_hint.get("kind"))
        .and_then(Value::as_str)
        .is_some());
}

#[test]
fn sarif_report_matches_expected_shape() {
    let source = "STATE = []\n\ndef f(items=[]):\n    print(items)\n    return STATE\n";
    let report = check_code(source, "memory.py", &CheckerConfig::default());
    let json = render_report(&report, OutputFormat::Sarif).unwrap();
    let value: Value = serde_json::from_str(&json).unwrap();

    assert_eq!(value.get("version").and_then(Value::as_str), Some("2.1.0"));
    assert!(value.get("runs").and_then(Value::as_array).is_some());

    let first_run = &value.get("runs").and_then(Value::as_array).unwrap()[0];
    assert!(first_run
        .get("tool")
        .and_then(|tool| tool.get("driver"))
        .and_then(Value::as_object)
        .is_some());
    assert!(first_run
        .get("results")
        .and_then(Value::as_array)
        .is_some_and(|results| !results.is_empty()));
    assert!(first_run
        .get("artifacts")
        .and_then(Value::as_array)
        .is_some_and(|artifacts| !artifacts.is_empty()));
}

#[test]
fn sarif_report_includes_custom_rule_metadata() {
    let config = CheckerConfig {
        enabled_rules: Some(BTreeSet::from(["no_eval".parse::<RuleId>().unwrap()])),
        custom_rules: vec![CustomRuleConfig {
            id: "no_eval".parse::<RuleId>().unwrap(),
            target: CustomRuleTarget::Call,
            match_type: CustomRuleMatchType::default(),
            patterns: vec!["eval".to_owned()],
            message: "危険な呼び出し `{match}` を禁止しています。".to_owned(),
            severity: Severity::Warning,
            hint: Some("`{match}` を安全な parser へ置き換えてください。".to_owned()),
            enabled: true,
        }],
        ..CheckerConfig::default()
    };

    let report = check_code(
        "def f(expr):\n    return eval(expr)\n",
        "memory.py",
        &config,
    );
    let json = render_report(&report, OutputFormat::Sarif).unwrap();
    let value: Value = serde_json::from_str(&json).unwrap();
    let rules = value["runs"][0]["tool"]["driver"]["rules"]
        .as_array()
        .unwrap();

    let custom_rule = rules
        .iter()
        .find(|rule| rule["id"].as_str() == Some("no_eval"))
        .unwrap();

    assert!(custom_rule["shortDescription"]["text"]
        .as_str()
        .is_some_and(|text| text.contains("カスタムルール `no_eval`")));
    assert_eq!(
        custom_rule["help"]["text"].as_str(),
        Some("設定ファイルで定義した指摘内容に従って修正してください。")
    );
}
