// Integration tests for vocabulary expansion: political/regional proper nouns
// and additional IT/software terminology (Tier 4.1 + 4.2).
//
// Uses the full embedded ruleset via Scanner to ensure new rules integrate
// correctly with existing scanning logic.

use zhtw_mcp::engine::scan::Scanner;
use zhtw_mcp::rules::ruleset::{IssueType, PoliticalStance, Profile, Ruleset};

/// Build a scanner from the embedded ruleset (same as the MCP server uses).
fn full_scanner() -> Scanner {
    let json_str = include_str!("../assets/ruleset.json");
    let ruleset: Ruleset = serde_json::from_str(json_str).unwrap();
    Scanner::new(ruleset.spelling_rules, ruleset.case_rules)
}

// ---------------------------------------------------------------------------
// 4.2: Political / regional proper nouns
// ---------------------------------------------------------------------------

#[test]
fn country_laos() {
    let scanner = full_scanner();
    let issues = scanner.scan("老撾是東南亞國家").issues;
    assert_eq!(issues.len(), 1);
    assert_eq!(issues[0].found, "老撾");
    assert!(issues[0].suggestions.contains(&"寮國".to_string()));
}

#[test]
fn country_new_zealand() {
    let scanner = full_scanner();
    let issues = scanner.scan("他移民到新西蘭").issues;
    assert_eq!(issues.len(), 1);
    assert_eq!(issues[0].found, "新西蘭");
    assert!(issues[0].suggestions.contains(&"紐西蘭".to_string()));
}

#[test]
fn country_italy() {
    let scanner = full_scanner();
    let issues = scanner.scan("意大利的美食很有名").issues;
    assert_eq!(issues.len(), 1);
    assert_eq!(issues[0].found, "意大利");
    assert!(issues[0].suggestions.contains(&"義大利".to_string()));
}

#[test]
fn country_saudi() {
    let scanner = full_scanner();
    let issues = scanner.scan("沙特的石油產量很高").issues;
    assert_eq!(issues.len(), 1);
    assert_eq!(issues[0].found, "沙特");
    assert!(issues[0].suggestions.contains(&"沙烏地".to_string()));
}

#[test]
fn org_asean() {
    let scanner = full_scanner();
    let issues = scanner.scan("東盟峰會在曼谷舉行").issues;
    assert_eq!(issues.len(), 1);
    assert_eq!(issues[0].found, "東盟");
    assert!(issues[0].suggestions.contains(&"東協".to_string()));
    assert_eq!(issues[0].rule_type, IssueType::PoliticalColoring);
}

#[test]
fn org_commonwealth() {
    let scanner = full_scanner();
    let issues = scanner.scan("英聯邦有五十多個成員國").issues;
    assert_eq!(issues.len(), 1);
    assert_eq!(issues[0].found, "英聯邦");
    assert!(issues[0].suggestions.contains(&"大英國協".to_string()));
    assert_eq!(issues[0].rule_type, IssueType::PoliticalColoring);
}

#[test]
fn country_qatar() {
    let scanner = full_scanner();
    let issues = scanner.scan("卡塔爾世界杯在冬天舉行").issues;
    assert_eq!(issues.len(), 1);
    assert_eq!(issues[0].found, "卡塔爾");
    assert!(issues[0].suggestions.contains(&"卡達".to_string()));
}

#[test]
fn country_georgia() {
    let scanner = full_scanner();
    let issues = scanner.scan("格魯吉亞位於高加索地區").issues;
    assert_eq!(issues.len(), 1);
    assert_eq!(issues[0].found, "格魯吉亞");
    assert!(issues[0].suggestions.contains(&"喬治亞".to_string()));
}

#[test]
fn country_croatia() {
    let scanner = full_scanner();
    let issues = scanner.scan("克羅地亞的足球隊很強").issues;
    assert_eq!(issues.len(), 1);
    assert_eq!(issues[0].found, "克羅地亞");
    assert!(issues[0].suggestions.contains(&"克羅埃西亞".to_string()));
}

// Multiple political nouns in one sentence
#[test]
fn multiple_countries_in_prose() {
    let scanner = full_scanner();
    let issues = scanner.scan("意大利和新西蘭簽署了貿易協定").issues;
    assert_eq!(issues.len(), 2);
    let founds: Vec<&str> = issues.iter().map(|i| i.found.as_str()).collect();
    assert!(founds.contains(&"意大利"));
    assert!(founds.contains(&"新西蘭"));
}

// Clean text should not trigger
#[test]
fn tw_country_names_clean() {
    let scanner = full_scanner();
    let issues = scanner.scan("義大利和紐西蘭簽署了貿易協定").issues;
    // No political/country issues (might have other punctuation issues)
    let country_issues: Vec<_> = issues
        .iter()
        .filter(|i| i.found == "義大利" || i.found == "紐西蘭")
        .collect();
    assert!(country_issues.is_empty());
}

// ---------------------------------------------------------------------------
// 4.1: IT/software terminology
// ---------------------------------------------------------------------------

#[test]
fn it_probability() {
    let scanner = full_scanner();
    let issues = scanner.scan("這個事件的概率很低").issues;
    assert_eq!(issues.len(), 1);
    assert_eq!(issues[0].found, "概率");
    assert!(issues[0].suggestions.contains(&"機率".to_string()));
}

#[test]
fn it_probability_in_prose() {
    let scanner = full_scanner();
    let text = "根據貝氏定理計算後驗概率分布";
    let issues = scanner.scan(text).issues;
    let prob_issues: Vec<_> = issues.iter().filter(|i| i.found == "概率").collect();
    assert_eq!(prob_issues.len(), 1);
}

// Existing IT rules still work after expansion
#[test]
fn existing_it_rules_still_fire() {
    let scanner = full_scanner();
    let issues = scanner.scan("這個軟件需要更新").issues;
    assert!(issues.iter().any(|i| i.found == "軟件"));
}

#[test]
fn namespace_not_flagged() {
    // 命名空間 is already standard TW usage; the incorrect cross_strait
    // rule mapping it to 名稱空間/名字空間 was removed.
    let scanner = full_scanner();
    let issues = scanner.scan("使用命名空間隔離模組").issues;
    assert!(
        issues.iter().all(|i| i.found != "命名空間"),
        "命名空間 is correct TW and must NOT be flagged"
    );
}

#[test]
fn row_column_vector_terms_not_swapped() {
    // 列/行 terms are valid Taiwanese terms in row/column contexts.
    // Generic math clues cannot prove the author meant the PRC sense, and
    // swapping them can reverse the mathematical meaning.
    let scanner = full_scanner();
    let issues = scanner
        .scan("矩陣的每一列可視為列向量；矩陣的每一行可視為行向量。初等列變換與初等行變換不同。")
        .issues;
    assert!(
        issues.iter().all(|i| {
            !matches!(
                i.found.as_str(),
                "列向量" | "行向量" | "初等列變換" | "初等行變換"
            )
        }),
        "row/column terms must not be swapped, got {:?}",
        issues.iter().map(|i| &i.found).collect::<Vec<_>>()
    );
}

#[test]
fn standard_composite_function_term_not_flagged() {
    // 合成函數 is standard mathematical usage in Taiwan; do not rewrite the
    // 合成 prefix to 複合 solely because 函數 is nearby.
    let scanner = full_scanner();
    let issues = scanner.scan("合成函數是函數之間的標準運算。").issues;
    assert!(
        issues.iter().all(|i| i.found != "合成"),
        "合成函數 must not be flagged, got {:?}",
        issues.iter().map(|i| &i.found).collect::<Vec<_>>()
    );
}

// Profile interaction: strict catches all + variants
#[test]
fn political_nouns_fire_under_all_profiles() {
    let scanner = full_scanner();
    for profile in Profile::ALL {
        let issues = scanner.scan_profiled("老撾是東南亞國家", *profile).issues;
        assert!(
            issues.iter().any(|i| i.found == "老撾"),
            "Profile {:?} should flag 老撾",
            profile
        );
    }
}

// ---------------------------------------------------------------------------
// 4.3: Context clues on ambiguous rules
// ---------------------------------------------------------------------------

#[test]
fn context_clues_present_on_ambiguous_rules() {
    let json_str = include_str!("../assets/ruleset.json");
    let ruleset: Ruleset = serde_json::from_str(json_str).unwrap();
    let ambiguous_terms = [
        // Existing 4
        "程序", "質量", "接口", "並行", // High-risk: common non-IT usage in Taiwan
        "函數", "分配", "刷新", "地址", "循環", "菜單", "證書",
        // Moderate-risk: domain-specific ambiguity
        "交互", "場景", "日誌", "嚮導", "語句", "社交",
        // Newly contextualized IT/UI terms must also be clue-gated
        "保存", "創建", "導航", "打開", "推遲", "搜索", "添加", "觸摸", "設置", "運行", "響應",
        "高級",
    ];
    for term in &ambiguous_terms {
        let rule = ruleset
            .spelling_rules
            .iter()
            .find(|r| r.from == *term && r.english.is_some())
            .unwrap_or_else(|| panic!("ambiguous rule for {} not found", term));
        assert!(
            rule.context_clues.is_some(),
            "Rule for {} should have context_clues",
            term
        );
        let clues = rule.context_clues.as_ref().unwrap();
        assert!(
            clues.len() >= 2,
            "Rule for {} should have at least 2 context clues, got {}",
            term,
            clues.len()
        );
    }
}

#[test]
fn context_clues_propagated_to_issue() {
    let scanner = full_scanner();
    let issues = scanner.scan("我需要編寫一個程序來執行").issues;
    let prog_issues: Vec<_> = issues.iter().filter(|i| i.found == "程序").collect();
    assert_eq!(prog_issues.len(), 1);
    assert!(
        prog_issues[0].context_clues.is_some(),
        "Issue for 程序 should carry context_clues"
    );
}

#[test]
fn fixer_lexical_safe_skips_context_clue_rules() {
    use zhtw_mcp::fixer::{apply_fixes, FixMode};

    let scanner = full_scanner();
    let text = "我需要編寫一個程序來執行";
    let issues = scanner.scan(text).issues;
    // LexicalSafe should skip 程序 because it has context_clues
    let result = apply_fixes(text, &issues, FixMode::LexicalSafe, &[]);
    assert!(
        result.text.contains("程序"),
        "LexicalSafe should not replace 程序 (has context_clues)"
    );
}

#[test]
fn fixer_lexical_contextual_with_segmenter_applies_when_clues_match() {
    use zhtw_mcp::engine::segment::Segmenter;
    use zhtw_mcp::fixer::{apply_fixes_with_context, FixMode};

    let json_str = include_str!("../assets/ruleset.json");
    let ruleset: Ruleset = serde_json::from_str(json_str).unwrap();
    let scanner = Scanner::new(ruleset.spelling_rules.clone(), ruleset.case_rules);
    let segmenter = Segmenter::from_rules(&ruleset.spelling_rules);

    let text = "我需要編寫一個程序來執行";
    let issues = scanner.scan(text).issues;
    let result = apply_fixes_with_context(
        text,
        &issues,
        FixMode::LexicalContextual,
        &[],
        Some(&segmenter),
    );
    assert!(
        result.text.contains("程式"),
        "LexicalContextual with segmenter should replace 程序->程式 when clues match"
    );
}

#[test]
fn fixer_lexical_contextual_with_segmenter_skips_when_no_clues() {
    use zhtw_mcp::engine::segment::Segmenter;
    use zhtw_mcp::fixer::{apply_fixes_with_context, FixMode};

    let json_str = include_str!("../assets/ruleset.json");
    let ruleset: Ruleset = serde_json::from_str(json_str).unwrap();
    let scanner = Scanner::new(ruleset.spelling_rules.clone(), ruleset.case_rules);
    let segmenter = Segmenter::from_rules(&ruleset.spelling_rules);

    let text = "這個程序很複雜";
    let issues = scanner.scan(text).issues;
    let result = apply_fixes_with_context(
        text,
        &issues,
        FixMode::LexicalContextual,
        &[],
        Some(&segmenter),
    );
    assert!(
        result.text.contains("程序"),
        "LexicalContextual should skip 程序 when context clues are insufficient"
    );
}

// English anchor present for disambiguation
#[test]
fn country_rules_have_english_field() {
    let json_str = include_str!("../assets/ruleset.json");
    let ruleset: Ruleset = serde_json::from_str(json_str).unwrap();
    let country_terms = ["老撾", "沙特", "新西蘭", "意大利", "卡塔爾", "格魯吉亞"];
    for term in &country_terms {
        let rule = ruleset
            .spelling_rules
            .iter()
            .find(|r| r.from == *term)
            .unwrap_or_else(|| panic!("rule for {} not found", term));
        assert!(
            rule.english.is_some(),
            "Rule for {} should have english field",
            term
        );
    }
}

// ---------------------------------------------------------------------------
// 6.2: Political stance profiles
// ---------------------------------------------------------------------------

#[test]
fn roc_centric_flags_all_political_terms() {
    let scanner = full_scanner();
    let cfg = Profile::Base.config();
    // RocCentric (default): 內地 should be flagged
    let excluded = vec![];
    let issues = scanner
        .scan_with_config("這是中國內地的情況", &excluded, cfg)
        .issues;
    assert!(
        issues.iter().any(|i| i.found == "內地"),
        "RocCentric should flag 內地"
    );
}

#[test]
fn roc_centric_flags_asean() {
    let scanner = full_scanner();
    let cfg = Profile::Base.config();
    let issues = scanner.scan_with_config("東盟峰會", &[], cfg).issues;
    assert!(
        issues.iter().any(|i| i.found == "東盟"),
        "RocCentric should flag 東盟"
    );
}

#[test]
fn international_skips_identity_terms() {
    let scanner = full_scanner();
    let cfg = Profile::Base
        .config()
        .with_stance(PoliticalStance::International);
    // International: 內地 should NOT be flagged (identity-loaded)
    let issues = scanner
        .scan_with_config("這是中國內地的情況", &[], cfg)
        .issues;
    assert!(
        !issues.iter().any(|i| i.found == "內地"),
        "International should NOT flag 內地"
    );
}

#[test]
fn international_keeps_org_names() {
    let scanner = full_scanner();
    let cfg = Profile::Base
        .config()
        .with_stance(PoliticalStance::International);
    // International: 東盟 should still be flagged (org name)
    let issues = scanner.scan_with_config("東盟峰會", &[], cfg).issues;
    assert!(
        issues.iter().any(|i| i.found == "東盟"),
        "International should still flag 東盟"
    );
}

#[test]
fn neutral_suppresses_all_political() {
    let scanner = full_scanner();
    let cfg = Profile::Base.config().with_stance(PoliticalStance::Neutral);
    // Neutral: neither 內地 nor 東盟 should be flagged
    let issues = scanner.scan_with_config("內地的東盟峰會", &[], cfg).issues;
    let political: Vec<_> = issues
        .iter()
        .filter(|i| i.rule_type == IssueType::PoliticalColoring)
        .collect();
    assert!(
        political.is_empty(),
        "Neutral should suppress all political_coloring rules, got {:?}",
        political.iter().map(|i| &i.found).collect::<Vec<_>>()
    );
}

#[test]
fn neutral_still_flags_cross_strait() {
    let scanner = full_scanner();
    let cfg = Profile::Base.config().with_stance(PoliticalStance::Neutral);
    // Neutral suppresses political but NOT cross_strait vocabulary
    let issues = scanner
        .scan_with_config("這個軟件需要更新", &[], cfg)
        .issues;
    assert!(
        issues.iter().any(|i| i.found == "軟件"),
        "Neutral should still flag cross_strait terms like 軟件"
    );
}

#[test]
fn stance_allows_rule_logic() {
    // Unit-level check for allows_rule
    assert!(PoliticalStance::RocCentric.allows_rule("內地"));
    assert!(PoliticalStance::RocCentric.allows_rule("東盟"));
    assert!(!PoliticalStance::International.allows_rule("內地"));
    assert!(!PoliticalStance::International.allows_rule("大陸同胞"));
    assert!(!PoliticalStance::International.allows_rule("祖國"));
    assert!(PoliticalStance::International.allows_rule("東盟"));
    assert!(PoliticalStance::International.allows_rule("英聯邦"));
    assert!(!PoliticalStance::Neutral.allows_rule("內地"));
    assert!(!PoliticalStance::Neutral.allows_rule("東盟"));
}

// ---------------------------------------------------------------------------
// Context-clue gate: scanner-level false-positive suppression
// ---------------------------------------------------------------------------

#[test]
fn scanner_suppresses_zhichi_in_political_context() {
    // 支持 in a political context (no IT context clues) must NOT fire.
    let scanner = full_scanner();
    let issues = scanner
        .scan("許多代理商擅自發表「支持統一」的言論，母公司並未反對。")
        .issues;
    assert!(
        issues.iter().all(|i| i.found != "支持"),
        "支持 must not fire in non-IT political context, got {:?}",
        issues.iter().map(|i| &i.found).collect::<Vec<_>>()
    );
}

#[test]
fn scanner_fires_zhichi_in_it_context() {
    // 支持 next to IT context clues (瀏覽器) must fire.
    let scanner = full_scanner();
    let issues = scanner.scan("此瀏覽器支持 WebGL 渲染。").issues;
    assert!(
        issues.iter().any(|i| i.found == "支持"),
        "支持 must fire when IT context clue 瀏覽器 is nearby"
    );
}

#[test]
fn scanner_suppresses_shengming_in_political_context() {
    // 聲明 as a public statement (no programming context clues) must NOT fire.
    let scanner = full_scanner();
    let issues = scanner
        .scan("除非母公司曾發表聲明反對代理商的言論，否則視兩者為同一立場。")
        .issues;
    assert!(
        issues.iter().all(|i| i.found != "聲明"),
        "聲明 must not fire in non-programming context, got {:?}",
        issues.iter().map(|i| &i.found).collect::<Vec<_>>()
    );
}

#[test]
fn scanner_fires_shengming_in_programming_context() {
    // 聲明 next to a programming context clue (變數) must fire.
    let scanner = full_scanner();
    let issues = scanner.scan("在函式開頭的變數聲明需要明確型別。").issues;
    assert!(
        issues.iter().any(|i| i.found == "聲明"),
        "聲明 must fire when programming context clue 變數 is nearby"
    );
}

#[test]
fn scanner_suppresses_baocun_near_negative_clue() {
    // negative_context_clues: 食物, 文化, 遺產, 期限
    let scanner = full_scanner();
    let issues = scanner.scan("食物保存期限應標示清楚").issues;
    assert!(
        issues.iter().all(|i| i.found != "保存"),
        "保存 must not fire when negative clue '食物'/'期限' is nearby, got {:?}",
        issues.iter().map(|i| &i.found).collect::<Vec<_>>()
    );
}

#[test]
fn scanner_suppresses_baocun_in_general_prose() {
    let scanner = full_scanner();
    let issues = scanner.scan("警方保存證據以供調查").issues;
    assert!(
        issues.iter().all(|i| i.found != "保存"),
        "保存 must not fire without nearby IT clues, got {:?}",
        issues.iter().map(|i| &i.found).collect::<Vec<_>>()
    );
}

#[test]
fn scanner_fires_baocun_in_it_context() {
    let scanner = full_scanner();
    let issues = scanner.scan("請按下保存按鈕以保存檔案").issues;
    assert!(
        issues.iter().any(|i| i.found == "保存"),
        "保存 must fire when IT clues are nearby"
    );
}

#[test]
fn scanner_suppresses_daohang_near_negative_clue() {
    // negative_context_clues: 衛星, GPS, 汽車, 路線
    let scanner = full_scanner();
    let issues = scanner.scan("汽車導航系統使用衛星定位").issues;
    assert!(
        issues.iter().all(|i| i.found != "導航"),
        "導航 must not fire when negative clue '汽車'/'衛星' is nearby, got {:?}",
        issues.iter().map(|i| &i.found).collect::<Vec<_>>()
    );
}

#[test]
fn scanner_suppresses_daohang_in_general_prose() {
    let scanner = full_scanner();
    let issues = scanner.scan("這本書提供職涯導航與建議").issues;
    assert!(
        issues.iter().all(|i| i.found != "導航"),
        "導航 must not fire without nearby UI clues, got {:?}",
        issues.iter().map(|i| &i.found).collect::<Vec<_>>()
    );
}

#[test]
fn scanner_fires_daohang_in_ui_context() {
    let scanner = full_scanner();
    let issues = scanner.scan("網站導航選單需要重新設計").issues;
    assert!(
        issues.iter().any(|i| i.found == "導航"),
        "導航 must fire when UI clues are nearby"
    );
}

#[test]
fn scanner_suppresses_yunxing_near_negative_clue() {
    // negative_context_clues: 軌道, 列車, 班次
    let scanner = full_scanner();
    let issues = scanner.scan("列車運行時刻表已更新").issues;
    assert!(
        issues.iter().all(|i| i.found != "運行"),
        "運行 must not fire when negative clue '列車' is nearby, got {:?}",
        issues.iter().map(|i| &i.found).collect::<Vec<_>>()
    );
}

#[test]
fn scanner_suppresses_yunxing_in_general_prose() {
    let scanner = full_scanner();
    let issues = scanner.scan("這項計畫運行順利").issues;
    assert!(
        issues.iter().all(|i| i.found != "運行"),
        "運行 must not fire without nearby software clues, got {:?}",
        issues.iter().map(|i| &i.found).collect::<Vec<_>>()
    );
}

#[test]
fn scanner_fires_yunxing_in_software_context() {
    let scanner = full_scanner();
    let issues = scanner.scan("系統運行腳本後會重新啟動服務").issues;
    assert!(
        issues.iter().any(|i| i.found == "運行"),
        "運行 must fire when software clues are nearby"
    );
}

// ---------------------------------------------------------------------------
// 14.4: CS Terminology — 參數 must NOT be flagged (correct zh-TW for parameter)
// ---------------------------------------------------------------------------

#[test]
fn parameter_canshu_not_flagged() {
    // 參數 is the correct zh-TW term for "parameter"; the old rule incorrectly
    // flagged it as wrong.  After disabling that rule, it must not fire.
    let scanner = full_scanner();
    let issues = scanner.scan("函式的參數需要明確型別").issues;
    assert!(
        issues.iter().all(|i| i.found != "參數"),
        "參數 is correct zh-TW for 'parameter' and must NOT be flagged, got: {:?}",
        issues
            .iter()
            .filter(|i| i.found == "參數")
            .collect::<Vec<_>>()
    );
}

#[test]
fn argument_yinshu_not_affected() {
    // 實參 (CN for "argument") should still be flagged → 引數
    let scanner = full_scanner();
    let issues = scanner.scan("呼叫函式時的實參需要符合型別").issues;
    assert!(
        issues.iter().any(|i| i.found == "實參"),
        "實參 (CN term for 'argument') should still be flagged"
    );
}

// ---------------------------------------------------------------------------
// 宏 rule: exceptions for compound words where 宏 means "grand/vast"
// ---------------------------------------------------------------------------

#[test]
fn macro_hong_fires_standalone() {
    let scanner = full_scanner();
    // 宏 rule is clue-gated; needs a macro-related clue nearby.
    let issues = scanner.scan("這個宏是用 #define 展開的").issues;
    assert!(
        issues.iter().any(|i| i.found == "宏"),
        "standalone 宏 (macro) must be flagged when macro clues present"
    );
}

#[test]
fn macro_hong_requires_macro_clue() {
    let scanner = full_scanner();
    let issues = scanner.scan("這個宏定義了一個函式").issues;
    assert!(
        issues.iter().all(|i| i.found != "宏"),
        "宏 should stay suppressed without configured macro clues"
    );
}

#[test]
fn macro_hong_skips_hongguan() {
    // 宏觀 = macroscopic — NOT a programming macro
    let scanner = full_scanner();
    let issues = scanner.scan("宏觀經濟學是重要的學科").issues;
    assert!(
        issues.iter().all(|i| i.found != "宏"),
        "宏觀 must not trigger 宏→巨集 rule, got: {:?}",
        issues
            .iter()
            .filter(|i| i.found == "宏")
            .collect::<Vec<_>>()
    );
}

#[test]
fn macro_hong_skips_hongwei() {
    // 宏偉 = grand/imposing — NOT a programming macro
    let scanner = full_scanner();
    let issues = scanner.scan("這是一座宏偉的建築").issues;
    assert!(
        issues.iter().all(|i| i.found != "宏"),
        "宏偉 must not trigger 宏→巨集 rule, got: {:?}",
        issues
            .iter()
            .filter(|i| i.found == "宏")
            .collect::<Vec<_>>()
    );
}

#[test]
fn macro_hong_skips_huihong() {
    // 恢宏 = magnificent — 宏 at position 1, not position 0
    let scanner = full_scanner();
    let issues = scanner.scan("氣勢恢宏的場面").issues;
    assert!(
        issues.iter().all(|i| i.found != "宏"),
        "恢宏 must not trigger 宏→巨集 rule, got: {:?}",
        issues
            .iter()
            .filter(|i| i.found == "宏")
            .collect::<Vec<_>>()
    );
}

#[test]
fn macro_hong_skips_hongqi() {
    // 宏碁 = Acer (Taiwan brand name)
    let scanner = full_scanner();
    let issues = scanner.scan("宏碁電腦是台灣品牌").issues;
    assert!(
        issues.iter().all(|i| i.found != "宏"),
        "宏碁 must not trigger 宏→巨集 rule, got: {:?}",
        issues
            .iter()
            .filter(|i| i.found == "宏")
            .collect::<Vec<_>>()
    );
}

#[test]
fn monolithic_kernel_suggestion() {
    // 宏內核 should suggest 單體式核心, not 單核心
    let scanner = full_scanner();
    let issues = scanner.scan("Linux 是宏內核架構").issues;
    let hit = issues.iter().find(|i| i.found == "宏內核");
    assert!(hit.is_some(), "宏內核 must be flagged");
    assert!(
        hit.unwrap().suggestions.contains(&"單體式核心".to_string()),
        "宏內核 should suggest 單體式核心, got: {:?}",
        hit.unwrap().suggestions
    );
}
