//! 模型探测器。
//!
//! 给定模型名,产出完整 `ModelConfig`(4 维度 + 路由控制 + 探测来源标注)。
//!
//! 多源探测,高优先源命中即返(短路):
//!   1. 内置预设表精确匹配(name 完全相等) → `ProbeSource::PresetTable`
//!   2. 内置预设表模糊匹配(子串包含)       → `ProbeSource::PresetTable`
//!   3. 模型名启发式推断(命名模式)         → `ProbeSource::Heuristic`
//!   4. 默认值兜底(`ModelConfig::with_defaults`) → `ProbeSource::Default`
//!
//! 设计来源:docs/02-架构设计/已编号方案/F-01-模型能力系统与智能路由设计-2026-06-16.md §4
//! (设计文档 §4.1 写的是"启发式先行 + 预设表合并";本实施按阶段 2 任务规格
//!  收敛为"预设优先 > 启发式"链 — 预设表精确数据可信度高于命名猜测,优先短路)。

use df_ai_core::model::{ModelConfig, ProbeSource};

// 纯逻辑子模块(预设表加载 / 启发式推断 / 词素判定)抽离至此 —
// private mod,crate 内复用,外部经 model_probe::probe 间接访问(对齐 model_fetch_helpers)。
use crate::model_probe_helpers::{heuristic_infer, presets};

// ────────────────────────────────────────────────────────────
// 公共入口
// ────────────────────────────────────────────────────────────

/// 探测单个模型,返回完整 `ModelConfig`(已填充 `probe_source`)。
///
/// 多源探测顺序(高优先源命中即返):
/// 1. 预设表精确匹配(`model_id` 完全相等,大小写敏感)
/// 2. 预设表模糊匹配(`model_id` 双向子串包含,大小写不敏感)
/// 3. 启发式推断(模型名命名模式)
/// 4. 默认值兜底
///
/// 返回的 `ModelConfig.probe_source` 标注实际命中来源。
pub fn probe(model_id: &str) -> ModelConfig {
    // 1. 预设表精确匹配
    if let Some(mut hit) = presets().iter().find(|m| m.model_id == model_id).cloned() {
        hit.probe_source = Some(ProbeSource::PresetTable);
        return hit;
    }

    // 2. 预设表模糊匹配(双向子串包含,大小写不敏感)
    // 多个候选命中时,选预设 model_id 最长者(最具体:glm-4v > glm-4)。
    let needle = model_id.to_lowercase();
    let fuzzy = presets()
        .iter()
        .filter(|m| {
            let cand = m.model_id.to_lowercase();
            !cand.is_empty() && (cand.contains(&needle) || needle.contains(&cand))
        })
        .max_by_key(|m| m.model_id.len());

    if let Some(mut hit) = fuzzy.cloned() {
        // 模糊命中:保留预设维度,但用入参的实际模型名覆盖 model_id(避免回写错名)
        hit.model_id = model_id.to_string();
        hit.probe_source = Some(ProbeSource::PresetTable);
        return hit;
    }

    // 3. 启发式推断
    let mut heuristic = heuristic_infer(model_id);
    heuristic.probe_source = Some(ProbeSource::Heuristic);
    heuristic
}

// ────────────────────────────────────────────────────────────
// 测试
// ────────────────────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use super::*;
    use df_ai_core::model::{Capability, CostTier, IntelligenceTier, Modality, ProbeSource};

    // ── 预设表加载 ──

    #[test]
    fn presets_loaded_and_nonempty() {
        let p = presets();
        assert!(!p.is_empty(), "预设表应非空");
        // 确保每个预设都有 model_id
        assert!(p.iter().all(|m| !m.model_id.is_empty()));
    }

    #[test]
    fn presets_contain_expected_models() {
        let p = presets();
        let ids: Vec<&str> = p.iter().map(|m| m.model_id.as_str()).collect();
        assert!(ids.contains(&"glm-4"), "glm-4 应在预设表: {ids:?}");
        assert!(ids.contains(&"glm-4-flash"));
        assert!(ids.contains(&"glm-4v"));
        assert!(ids.contains(&"gpt-4o"));
        assert!(ids.contains(&"claude-3-5-sonnet-20241022"));
    }

    // ── 预设精确匹配 ──

    #[test]
    fn probe_preset_exact_match_glm4() {
        let m = probe("glm-4");
        assert_eq!(m.model_id, "glm-4");
        assert_eq!(m.probe_source, Some(ProbeSource::PresetTable));
        assert_eq!(m.modalities, vec![Modality::Text]);
        // 预设表不写死 cost_tier/intelligence,由 serde default 兜底中性值
        assert_eq!(m.intelligence, IntelligenceTier::Standard);
        assert_eq!(m.cost_tier, CostTier::Medium);
    }

    #[test]
    fn probe_preset_exact_match_glm4v_has_vision() {
        let m = probe("glm-4v");
        assert_eq!(m.probe_source, Some(ProbeSource::PresetTable));
        assert!(m.modalities.contains(&Modality::Vision));
    }

    #[test]
    fn probe_preset_exact_match_deepseek_coder_has_codegen() {
        let m = probe("deepseek-coder");
        assert_eq!(m.probe_source, Some(ProbeSource::PresetTable));
        assert!(m.capabilities.contains(&Capability::CodeGen));
    }

    #[test]
    fn probe_preset_exact_match_embedding_no_text_modality() {
        let m = probe("embedding-3");
        assert_eq!(m.probe_source, Some(ProbeSource::PresetTable));
        assert!(m.capabilities.contains(&Capability::Embedding));
        assert!(
            !m.modalities.contains(&Modality::Text),
            "embedding 模型不应有 Text 模态"
        );
    }

    // ── 预设表:已知多模态模型(命名无 vision 词素,靠预设显式标 vision) ──
    // SenseNova flash-lite 原生多模态(商汤文档:支持图像输入理解),
    // 但模型名不含 vision/vl/-v 词素,启发式 has_vision_token 无法识别 →
    // 必须靠预设表显式标 vision,否则 modalities 漏 vision 致路由误判。
    #[test]
    fn probe_preset_sensenova_flash_lite_has_vision() {
        let m = probe("sensenova-6.7-flash-lite");
        assert_eq!(m.probe_source, Some(ProbeSource::PresetTable));
        assert!(
            m.modalities.contains(&Modality::Vision),
            "sensenova-6.7-flash-lite 应有 Vision(原生多模态): {:?}",
            m.modalities
        );
        assert!(m.modalities.contains(&Modality::Text));
        // context_window 应为预设的 256K
        assert_eq!(m.context_window, 262144);
    }

    #[test]
    fn probe_preset_deepseek_v4_flash_text_only() {
        // SenseNova 上的 deepseek-v4-flash:纯文本对话 + 1M 上下文,无 vision
        let m = probe("deepseek-v4-flash");
        assert_eq!(m.probe_source, Some(ProbeSource::PresetTable));
        assert_eq!(m.modalities, vec![Modality::Text]);
        assert_eq!(m.context_window, 1_048_576);
    }

    // ── 预设模糊匹配 ──

    #[test]
    fn probe_preset_fuzzy_match_glm4v_variant() {
        // "glm-4v-x" 不在预设表精确命中,但 "glm-4v" 是其子串 → 模糊命中
        let m = probe("glm-4v-x");
        assert_eq!(m.probe_source, Some(ProbeSource::PresetTable));
        assert_eq!(m.model_id, "glm-4v-x", "模糊命中后 model_id 应用入参名");
        assert!(
            m.modalities.contains(&Modality::Vision),
            "应继承 glm-4v 的 vision 模态"
        );
    }

    // ── 启发式:Vision ──

    #[test]
    fn heuristic_vision_from_v_suffix() {
        // glm-4v 不走启发式(精确命中预设);用未知名验证启发式
        let m = probe("custom-model-v");
        assert_eq!(m.probe_source, Some(ProbeSource::Heuristic));
        assert!(
            m.modalities.contains(&Modality::Vision),
            "v 后缀应推断 Vision"
        );
    }

    #[test]
    fn heuristic_vision_from_vl_token() {
        let m = probe("qwen-vl-unknown");
        assert_eq!(m.probe_source, Some(ProbeSource::Heuristic));
        assert!(m.modalities.contains(&Modality::Vision));
    }

    #[test]
    fn heuristic_vision_from_vision_word() {
        let m = probe("some-vision-7b");
        assert_eq!(m.probe_source, Some(ProbeSource::Heuristic));
        assert!(m.modalities.contains(&Modality::Vision));
    }

    // ── 启发式:档位中性(flash/mini 不再猜 Lite) ──

    #[test]
    fn heuristic_flash_keeps_neutral_tier() {
        let m = probe("unknown-flash");
        assert_eq!(m.probe_source, Some(ProbeSource::Heuristic));
        // cost/intel 一律中性,不靠名字猜
        assert_eq!(m.intelligence, IntelligenceTier::Standard);
        assert_eq!(m.cost_tier, CostTier::Medium);
    }

    #[test]
    fn heuristic_mini_keeps_neutral_tier() {
        let m = probe("test-mini-model");
        assert_eq!(m.intelligence, IntelligenceTier::Standard);
        assert_eq!(m.cost_tier, CostTier::Medium);
    }

    // ── 启发式:CodeGen ──

    #[test]
    fn heuristic_codegen_from_code() {
        let m = probe("acme-code-7b");
        assert_eq!(m.probe_source, Some(ProbeSource::Heuristic));
        assert!(m.capabilities.contains(&Capability::CodeGen));
        // 对话模型仍应保留 ToolUse
        assert!(m.capabilities.contains(&Capability::ToolUse));
    }

    #[test]
    fn heuristic_codegen_from_coder() {
        let m = probe("acme-coder");
        assert!(m.capabilities.contains(&Capability::CodeGen));
    }

    // ── 启发式:Embedding(特殊路径:无 Text 模态) ──

    #[test]
    fn heuristic_embedding_no_text_modality() {
        let m = probe("acme-embedding-v3");
        assert_eq!(m.probe_source, Some(ProbeSource::Heuristic));
        assert!(m.capabilities.contains(&Capability::Embedding));
        assert!(!m.modalities.contains(&Modality::Text));
        assert!(!m.capabilities.contains(&Capability::ToolUse));
    }

    #[test]
    fn heuristic_embedding_from_text_embedding_prefix() {
        let m = probe("text-embedding-3-large");
        assert!(m.capabilities.contains(&Capability::Embedding));
        assert!(m.modalities.is_empty());
    }

    // ── 启发式:档位中性(pro/opus/4o 不再猜 Plus/Ultra/High) ──

    #[test]
    fn heuristic_pro_keeps_neutral_tier() {
        let m = probe("acme-pro");
        assert_eq!(m.intelligence, IntelligenceTier::Standard);
        assert_eq!(m.cost_tier, CostTier::Medium);
    }

    #[test]
    fn heuristic_opus_keeps_neutral_tier() {
        let m = probe("acme-opus");
        assert_eq!(m.intelligence, IntelligenceTier::Standard);
    }

    #[test]
    fn heuristic_4o_keeps_neutral_cost() {
        let m = probe("acme-4o");
        assert_eq!(m.cost_tier, CostTier::Medium);
    }

    // ── 启发式:默认(Standard/Medium/Text/ToolUse) ──

    #[test]
    fn heuristic_default_for_unknown_name() {
        let m = probe("acme-unknown-model");
        assert_eq!(m.probe_source, Some(ProbeSource::Heuristic));
        assert_eq!(m.modalities, vec![Modality::Text]);
        assert_eq!(m.capabilities, vec![Capability::ToolUse]);
        assert_eq!(m.intelligence, IntelligenceTier::Standard);
        assert_eq!(m.cost_tier, CostTier::Medium);
    }

    // ── 优先级链:预设精确 > 启发式 ──

    #[test]
    fn probe_preset_beats_heuristic() {
        // "glm-4-flash" 精确命中预设表;预设/启发式档位都中性,
        // 此处仅校验 source 标注为 PresetTable
        let m = probe("glm-4-flash");
        assert_eq!(m.probe_source, Some(ProbeSource::PresetTable));
    }

    #[test]
    fn probe_priority_chain_returns_config() {
        // 任何名字都应返回完整 ModelConfig,不 panic
        for name in ["glm-4", "unknown-xyz", "qwen-vl", "acme-embedding", "gpt-5"] {
            let m = probe(name);
            assert_eq!(m.model_id, name);
            assert!(
                m.probe_source.is_some(),
                "probe_source 应被填充: {name}"
            );
        }
    }
}
