//! 流式接收 LLM 响应

use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use std::time::Duration;

use tauri::{AppHandle, Emitter, Manager};
use tracing::warn;

use df_ai::provider::{CompletionRequest, LlmProvider};

use super::{AiChatEvent, ToolCallDraft};

/// 从 anyhow 错误中尽力提取 HTTP 状态码/错误分类，供诊断拼接。
///
/// `provider.stream()` 失败有两类来源：
/// - 业务层：provider 在 non-2xx 时 `bail!("LLM 流式 API 错误 {status}: {body}")`，原始文本已含状态码，
///   此处用正则从文本抠 `HTTP <code>` 或裸 `<code>`（provider 串已含 status code）。
/// - 传输层（reqwest，不直接命名以避免给 src-tauri 加依赖）：从 Display 文本识别
///   timeout/connect 关键词分类。
///
/// 返回 `(status_or_class, raw)`：status 取文本中首个三位数；否则按关键词给 timeout/connect/unknown。
/// 从字符串(非 anyhow)提取诊断信息,用于 mpsc channel 中来自专用线程的错误字符串。
fn extract_error_diag_from_str(raw: &str) -> (String, String) {
    // 1) 抠 HTTP 状态码
    const HTTP_CODES: &[&str] = &[
        "400", "401", "403", "404", "408", "409", "413",
        "422", "429", "500", "502", "503", "504",
    ];
    let chars: Vec<char> = raw.chars().collect();
    let n = chars.len();
    let mut i = 0;
    while i + 3 <= n {
        if chars[i].is_ascii_digit() && chars[i + 1].is_ascii_digit() && chars[i + 2].is_ascii_digit() {
            let code: String = chars[i..i + 3].iter().collect();
            let prev_ok = i == 0 || !chars[i - 1].is_ascii_digit();
            let next_ok = i + 3 == n || !chars[i + 3].is_ascii_digit();
            if prev_ok && next_ok && HTTP_CODES.contains(&code.as_str()) {
                return (format!("HTTP {}", code), raw.to_string());
            }
            i += 3;
            continue;
        }
        i += 1;
    }

    let lower = raw.to_lowercase();
    let class = if lower.contains("timeout") || lower.contains("超时") {
        "timeout"
    } else if lower.contains("connect") || lower.contains("dns") || lower.contains("resolve") {
        "connect"
    } else if lower.contains("timed out") {
        "timeout"
    } else {
        "unknown"
    };
    (class.to_string(), raw.to_string())
}

/// 从 anyhow::Error 提取诊断信息
#[allow(dead_code)]
fn extract_error_diag(e: &anyhow::Error) -> (String, String) {
    extract_error_diag_from_str(&e.to_string())
}

/// AiError 诊断消息的上下文，区分「建连/首字节失败」与「流中途断」两类。
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub(crate) enum DiagKind {
    /// `provider.stream()` 直接返回 Err：连接/鉴权/HTTP non-2xx 等
    #[allow(dead_code)]
    Init,
    /// 流已建立，next() 返回 Err：SSE 传输断/解析错等
    MidStream,
}

/// 拼接 AiError 的可读诊断文本（纯函数，便于单测）。
///
/// 格式：`[<provider_name>] <上下文>(<status_or_class>): <raw>`
/// - provider_name：`provider.name()`（anthropic 协议为 "anthropic-compat"，
///   openai 兼容为模型名）。当前 `LlmProvider` trait 未暴露 base_url/provider_type，
///   不改签名的前提下这是唯一可得的 provider 标识。
/// - status_or_class：`HTTP 4xx/5xx` 或 `timeout`/`connect`/`unknown` 分类。
/// - raw：anyhow 原始错误文本。
pub(crate) fn fmt_diag(provider_name: &str, kind: DiagKind, status_or_class: &str, raw: &str) -> String {
    let ctx = match kind {
        DiagKind::Init => "AI 调用失败",
        DiagKind::MidStream => "流式接收错误",
    };
    format!("[{}] {}({}): {}", provider_name, ctx, status_or_class, raw)
}

/// 流式接收结果。显式区分三类出口，避免调用方对 None 的歧义推断
/// (UX-2025-04 / CR-30-1 / CR-30-2 / 决策 F-260616-07 a1)。
pub(crate) enum StreamResult {
    /// 正常完成（流尽 + finished 信号，或用户主动停止）。incomplete=false。
    /// 调用方按正常流程入库 + emit AiCompleted。
    Complete {
        text: String,
        tool_calls: HashMap<u32, ToolCallDraft>,
        usage: df_ai::provider::TokenUsage,
        /// DeepSeek thinking 模式推理内容（需回传到下一轮请求）
        reasoning_content: Option<String>,
    },
    /// 流中途断（MidStream chunk Err / idle timeout / provider stream-error / 有 partial_text
    /// 但流尽未收到 finished）。已 emit 过 AiTextDelta（前端 currentText 已累积），
    /// **不 emit AiError**——调用方保文入库 + emit AiCompleted(incomplete=true) + 系统提示。
    /// 调用方**不重试**（决策 a1）。
    Partial {
        text: String,
        tool_calls: HashMap<u32, ToolCallDraft>,
        usage: df_ai::provider::TokenUsage,
        /// DeepSeek thinking 模式推理内容（部分累积）
        reasoning_content: Option<String>,
    },
    /// Init 失败（provider.stream() Err：建连/鉴权/HTTP non-2xx,或流建立后空文本各类中断）。
    /// UX-260618-15: **不再 emit AiError**——重试可恢复路径的 emit 权交调用方(agentic/mod.rs),
    /// 由 agentic 在重试耗尽/Fatal 时统一 emit 最终 AiError(单气泡),避免 N 次重试 push N+1 气泡。
    /// `retryable`: 据状态码分类(retry::is_status_retryable 镜像)：true=5xx/429/timeout/connect
    /// 可重试；false=4xx(非429)/鉴权/参数错 Fatal 立即放弃。调用方据此决定是否重试。
    /// `error`: 已格式化的错误文本(fmt_diag 拼接 provider 标识 + 上下文 + 状态 + 原文),
    /// 供 agentic 重试耗尽/Fatal 时 emit AiError 用,保留原诊断信息不丢失。
    InitFailed { retryable: bool, error: String },
}

/// 将 chunk 携带的 tool_calls delta 累加到累积表。
///
/// 每个 `ToolCallDelta` 按 `index` 归位到同一个 `ToolCallDraft`：
/// - `id` 覆盖(后到的 id 视为权威,匹配 OpenAI/Anthropic 协议行为)
/// - `function_name` / `function_arguments` 增量拼接(流式分片到达)
///
/// 抽取自 stream_llm 的 chunk match arm(原最深 7 层嵌套点),纯状态累加无 emit/return 副作用。
fn accumulate_tool_calls(
    tc_deltas: &[df_ai::provider::ToolCallDelta],
    tool_calls_acc: &mut HashMap<u32, ToolCallDraft>,
) {
    for tc_delta in tc_deltas {
        let draft = tool_calls_acc.entry(tc_delta.index).or_default();
        if let Some(id) = &tc_delta.id {
            draft.id = id.clone();
        }
        if let Some(name) = &tc_delta.function_name {
            draft.name.push_str(name);
        }
        if let Some(args) = &tc_delta.function_arguments {
            draft.args.push_str(args);
        }
    }
}

/// 流式接收 LLM 响应。
///
/// 三类异常处理（返回 StreamResult 显式区分出口）：
/// - Init 失败（provider.stream() Err）：UX-260618-15 **不 emit AiError**，返回
///   InitFailed{retryable, error}。retryable 据状态码分类(retry::is_status_retryable 镜像)：
///   5xx/429/timeout/connect=true，4xx(非429)/鉴权/参数错=false Fatal。调用方走流前重试(仅
///   retryable=true,≤max_retries 次, 复用 retry::backoff_delay 退避 + 30s 总预算),重试耗尽或
///   Fatal 时由调用方 emit 最终 AiError(error 字段携带诊断文本,单气泡聚合)。
/// - MidStream 失败（流已建立后断/timeout/err 事件/有 partial_text 但流尽未 finished）：
///   **不 emit AiError**，返回 Partial{...} 保文。调用方不重试，入库 + AiCompleted(incomplete)
///   + 系统提示网络中断。空文本仍返回 InitFailed{retryable=true}(无文可保,交重试,emit 权归调用方)。
/// - 用户停止（stop_flag）：返回 Complete（incomplete 语义非异常中断）。
///
/// 心跳与停止响应（B-260615-02 / B-260615-04）：单 `tokio::select!` 三分支
/// - `stream.next()`：正常 chunk 处理
/// - `heartbeat.tick()`（30s）：静默期（如工具执行后等下一轮首 chunk）emit `AiHeartbeat`，
///   前端 watchdog 据此 reset，区分「LLM 在跑」与「真断」（避免空气泡误报中断）
/// - `stop_notify.notified()`：用户点停止即时打断，不再等 chunk 到或 120s idle timeout
pub(crate) async fn stream_llm(
    provider: Arc<dyn LlmProvider>,
    request: CompletionRequest,
    app_handle: &AppHandle,
    stop_flag: &AtomicBool,
    notify: &tokio::sync::Notify,
    conv_id: &str,
) -> StreamResult {
    /// 流式读取空闲超时:超过此时长无任何 chunk 即判定连接已断
    /// BUG-2026-07-17:原 120s 太长,后端挂死时用户等太久。改为 45s,正常流式每 chunk 间隔
    /// 远不到 45s(即使模型思考间隙),超时即判为连接异常走保文/报错路径
    const STREAM_IDLE_TIMEOUT: Duration = Duration::from_secs(45);
    /// BUG-2026-07-08: 首 chunk 超时(等首字节)。
    /// 正常 provider 首 token <3s(即使 48 工具定义 + 长上下文),10s 足够宽容;
    /// 已有数据后中途静默仍用 STREAM_IDLE_TIMEOUT(120s,允许模型思考间隙)。
    const FIRST_CHUNK_TIMEOUT: Duration = Duration::from_secs(10);
    /// 心跳间隔：静默期向前端报「LLM 仍在跑」，reset watchdog
    const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(30);

    // ================================================================
    // BUG-2026-07-17 根治: 整个 LLM 流式请求运行在独立 OS 线程的
    // 专用 tokio runtime 上,通过 mpsc channel 将 chunk 流式送回主 runtime。
    // ================================================================
    //
    // 根因: Tauri 在 Windows 上使用单线程 tokio runtime。provider.stream()
    // 内部(eventsource-stream/reqwest TLS/网络建连)存在同步阻塞操作,
    // 阻塞唯一工作线程导致 tokio 计时器系统无法推进,timeout 不触发。
    //
    // 方案:
    // 1. 独立 OS 线程 + 多线程 tokio runtime 执行完整 LLM 请求
    //    (provider.stream + stream.next 循环),隔离同步阻塞。
    // 2. 通过 mpsc channel 将 StreamChunk 逐块传回主 runtime。
    // 3. reqwest Response/Stream 的生命周期不跨 runtime,避免
    //    "error decoding response body"(runtime 状态不兼容)。
    //
    // 资源: 专用线程运行至 LLM 流结束或 mpsc receiver 被 drop(超时/停止)。
    let (chunk_tx, mut chunk_rx) = tokio::sync::mpsc::channel::<
        std::result::Result<df_ai::provider::StreamChunk, String>
    >(64);
    let provider_for_thread = provider.clone();
    let conv_id_owned = conv_id.to_string();

    std::thread::spawn(move || {
        let rt = match tokio::runtime::Runtime::new() {
            Ok(rt) => rt,
            Err(e) => {
                tracing::error!(
                    conv_id = %conv_id_owned,
                    "[ai] 创建专用 tokio runtime 失败: {}",
                    e,
                );
                return;
            }
        };
        // BUG-2026-07-17 双重保险:在专用 runtime 内部也加 120s 超时。
        // 即使 provider.stream() 在专用线程上意外阻塞(理论不应发生,因专用 runtime
        // 多线程可推进独立 timer wheel),此 timeout 保证线程最终退出不永久挂起。
        rt.block_on(async move {
            use futures::StreamExt;
            let stream_result = tokio::time::timeout(
                std::time::Duration::from_secs(120),
                provider_for_thread.stream(request),
            ).await;
            match stream_result {
                Ok(Ok(mut stream)) => {
                    while let Some(chunk_result) = stream.next().await {
                        let sent = match chunk_result {
                            Ok(chunk) => chunk_tx.send(Ok(chunk)).await,
                            Err(e) => chunk_tx.send(Err(e.to_string())).await,
                        };
                        if sent.is_err() {
                            // receiver dropped (timeout/stop/error) — 停止发送
                            break;
                        }
                    }
                }
                Ok(Err(e)) => {
                    let _ = chunk_tx.send(Err(e.to_string())).await;
                }
                Err(_elapsed) => {
                    tracing::error!(
                        conv_id = %conv_id_owned,
                        "[ai] 专用 runtime 内部超时(120s),provider.stream 未返回"
                    );
                    let _ = chunk_tx.send(Err("LLM 调用超时(120s)".to_string())).await;
                }
            }
        });
    });

    // 在主 runtime 上通过 mpsc 接收 chunk,应用 timeout/heartbeat/stop 逻辑
    let mut full_text = String::new();
    let mut tool_calls_acc: HashMap<u32, ToolCallDraft> = HashMap::new();
    let mut finished_received = false;
    let mut stopped = false;
    let mut final_usage: Option<df_ai::provider::TokenUsage> = None;
    // BUG-260617-12: DeepSeek thinking 模式推理内容累积（多轮需回传）
    let mut reasoning_content_acc: Option<String> = None;

    // B-260615-15:heartbeat interval 提至 loop 外复用,避免每轮重建计时器
    // (每轮重建会丢已积累的节拍,且 interval 首次 tick 立即返回的特性会被误用)。
    // tokio interval 首 tick 立即返回——此处先丢弃首 tick,让心跳等满首个 30s 静默期才发
    // (心跳语义是"静默期仍在跑",循环入口立即报无意义且会与 rx.recv() 抢分支错过首 chunk)。
    let mut heartbeat = tokio::time::interval(HEARTBEAT_INTERVAL);
    heartbeat.tick().await; // 丢弃首 tick

    // BUG-2026-07-14 根治: wall-clock deadline 替代 select! 内 timeout 包裹。
    // idle_deadline 是绝对时刻:"下一次 chunk 必须在此前到达,否则判定超时"。
    // - 首 chunk 阶段: idle_deadline = now + 10s (FIRST_CHUNK_TIMEOUT)
    // - 收到首 chunk 后: idle_deadline = now + 120s (STREAM_IDLE_TIMEOUT),每次收到 chunk 更新
    // 心跳打断 select! 后回到循环顶部,deadline 检查在 select! 之前(不受竞争影响)。
    let mut idle_deadline = tokio::time::Instant::now() + FIRST_CHUNK_TIMEOUT;
    let mut first_chunk_done = false;

    loop {
        // B-260615-04:stop 即时打断。
        if stop_flag.load(std::sync::atomic::Ordering::SeqCst) {
            stopped = true;
            break;
        }

        // BUG-2026-07-14 根治: wall-clock deadline 检查(不依赖 select! timeout 语义)。
        if tokio::time::Instant::now() >= idle_deadline {
            if !first_chunk_done {
                return StreamResult::InitFailed {
                    retryable: true,
                    error: format!(
                        "流式响应超时（{}秒未收到首字节，服务无响应或请求体过大）",
                        FIRST_CHUNK_TIMEOUT.as_secs()
                    ),
                };
            }
            warn!(
                provider = %provider.name(),
                conv_id = %conv_id,
                text_len = full_text.len(),
                "[ai] 流中途 idle timeout(deadline),保文不重试(incomplete)",
            );
            return StreamResult::Partial {
                text: full_text,
                tool_calls: tool_calls_acc,
                usage: final_usage.unwrap_or_default(),
                reasoning_content: reasoning_content_acc,
            };
        }

        // 三分支 select!:
        // 1) chunk_rx.recv():来自专用线程的 LLM chunk (15s 保底 timeout,防 select! 死等)
        // 2) heartbeat.tick():静默期发 AiHeartbeat reset 前端 watchdog
        // 3) notify.notified():用户停止即时打断
        tokio::select! {
            chunk_result = tokio::time::timeout(
                Duration::from_secs(15),
                chunk_rx.recv()
            ) => {
                match chunk_result {
                    Err(_elapsed) => {
                        // 15s 保底 timeout 触发。正常路径不会到这里:心跳 30s 会先触发 select! 返回。
                        if !first_chunk_done {
                            return StreamResult::InitFailed {
                                retryable: true,
                                error: format!(
                                    "流式响应超时（{}秒未收到首字节，服务无响应或请求体过大）",
                                    FIRST_CHUNK_TIMEOUT.as_secs()
                                ),
                            };
                        }
                        warn!(
                            provider = %provider.name(),
                            conv_id = %conv_id,
                            text_len = full_text.len(),
                            "[ai] 流中途 idle timeout,保文不重试(incomplete)",
                        );
                        return StreamResult::Partial {
                            text: full_text,
                            tool_calls: tool_calls_acc,
                            usage: final_usage.unwrap_or_default(),
                            reasoning_content: reasoning_content_acc,
                        };
                    }
                    Ok(None) => break, // mpsc channel 关闭 = 专用线程 stream 结束
                    Ok(Some(Ok(chunk))) => {
                        // BUG-2026-07-14 根治: 收到 chunk 更新 idle_deadline。
                        if !first_chunk_done {
                            first_chunk_done = true;
                        }
                        idle_deadline = tokio::time::Instant::now() + STREAM_IDLE_TIMEOUT;
                        if !chunk.delta.is_empty() {
                            full_text.push_str(&chunk.delta);
                            // L3 emit 双写
                            let ev = AiChatEvent::AiTextDelta {
                                delta: chunk.delta,
                                conversation_id: Some(conv_id.to_string()),
                            };
                            let _ = app_handle.emit("ai-chat-event", ev.clone());
                            let _ = app_handle.state::<crate::state::AppState>().ai_event_bus.publish_event(ev);
                        }
                        if let Some(tc_deltas) = &chunk.tool_calls {
                            accumulate_tool_calls(tc_deltas, &mut tool_calls_acc);
                        }
                        if let Some(u) = &chunk.usage {
                            final_usage = Some(u.clone());
                        }
                        if let Some(ref rc) = chunk.reasoning_content {
                            reasoning_content_acc.get_or_insert_with(String::new).push_str(rc);
                        }
                        // provider 流式错误事件
                        if let Some(err_msg) = &chunk.error {
                            warn!(
                                provider = %provider.name(),
                                conv_id = %conv_id,
                                error = %err_msg,
                                text_len = full_text.len(),
                                "[ai] provider 流式错误事件",
                            );
                            if full_text.is_empty() && tool_calls_acc.is_empty() {
                                return StreamResult::InitFailed {
                                    retryable: true,
                                    error: fmt_diag(
                                        provider.name(),
                                        DiagKind::MidStream,
                                        "stream-error",
                                        err_msg,
                                    ),
                                };
                            }
                            return StreamResult::Partial {
                                text: full_text,
                                tool_calls: tool_calls_acc,
                                usage: final_usage.unwrap_or_default(),
                                reasoning_content: reasoning_content_acc,
                            };
                        }
                        if chunk.finished {
                            finished_received = true;
                            break;
                        }
                    }
                    Ok(Some(Err(err_str))) => {
                        // 专用线程上报的流错误(SSE 解析失败/HTTP 错误等)
                        let (status_or_class, raw) = extract_error_diag_from_str(&err_str);
                        warn!(
                            provider = %provider.name(),
                            status = %status_or_class,
                            conv_id = %conv_id,
                            error = %raw,
                            text_len = full_text.len(),
                            "[ai] 流式接收中途错误(from 专用线程)",
                        );
                        if full_text.is_empty() && tool_calls_acc.is_empty() {
                            return StreamResult::InitFailed {
                                retryable: classify_status_or_class(&status_or_class),
                                error: fmt_diag(
                                    provider.name(),
                                    DiagKind::MidStream,
                                    &status_or_class,
                                    &raw,
                                ),
                            };
                        }
                        return StreamResult::Partial {
                            text: full_text,
                            tool_calls: tool_calls_acc,
                            usage: final_usage.unwrap_or_default(),
                            reasoning_content: reasoning_content_acc,
                        };
                    }
                }
            }
            // 2) 心跳:静默期 30s 发 AiHeartbeat,前端 watchdog reset(B-260615-02)
            _ = heartbeat.tick() => {
                let ev = AiChatEvent::AiHeartbeat {
                    conversation_id: Some(conv_id.to_string()),
                };
                let _ = app_handle.emit("ai-chat-event", ev.clone());
                let _ = app_handle.state::<crate::state::AppState>().ai_event_bus.publish_event(ev);
            }
            // 3) 即时停止唤醒(B-260615-14)
            _ = notify.notified() => {
                if stop_flag.load(std::sync::atomic::Ordering::SeqCst) {
                    stopped = true;
                    break;
                }
            }
        }
    }

    // 用户停止
    if stopped {
        return StreamResult::Complete {
            text: full_text,
            tool_calls: tool_calls_acc,
            usage: final_usage.unwrap_or_default(),
            reasoning_content: reasoning_content_acc,
        };
    }

    // 断连检测
    if !finished_received {
        if full_text.is_empty() && tool_calls_acc.is_empty() {
            return StreamResult::InitFailed {
                retryable: true,
                error: "流式响应意外中断（未收到完成信号，已丢弃残缺响应）".to_string(),
            };
        }
        warn!(
            provider = %provider.name(),
            conv_id = %conv_id,
            text_len = full_text.len(),
            "[ai] 流尽未收到 finished 但有 partial_text,保文不重试(incomplete)",
        );
        return StreamResult::Partial {
            text: full_text,
            tool_calls: tool_calls_acc,
            usage: final_usage.unwrap_or_default(),
            reasoning_content: reasoning_content_acc,
        };
    }

    // 正常完成
    StreamResult::Complete {
        text: full_text,
        tool_calls: tool_calls_acc,
        usage: final_usage.unwrap_or_default(),
        reasoning_content: reasoning_content_acc,
    }
}

/// 据 extract_error_diag 产出的 status_or_class 字符串镜像 retry::is_status_retryable 分类。
///
/// status_or_class 取值: "HTTP 5xx"/"HTTP 4xx"(白名单码) / "timeout" / "connect" / "unknown"。
/// - HTTP 429 / 5xx → retryable=true(复用 retry::is_status_retryable 逻辑)
/// - HTTP 4xx(非429) → retryable=false Fatal(401/403/404/422 等)
/// - timeout / connect → retryable=true(瞬态,可重试)
/// - unknown / 其他 → retryable=true(保守可重试,避免误判 Fatal 错杀)
///
/// CR-30-1: 抽公共分类函数(决策 F-260616-07 a1 "复用 retry.rs 错误分类逻辑或镜像"),
/// 避免 stream_recv 与 agentic 各自重写状态码解析。
fn classify_status_or_class(status_or_class: &str) -> bool {
    use df_ai::retry::is_status_retryable;
    // 抠 status_or_class 内三位数字状态码("HTTP 401" → 401)
    let chars: Vec<char> = status_or_class.chars().collect();
    let mut i = 0;
    let n = chars.len();
    while i + 3 <= n {
        if chars[i].is_ascii_digit()
            && chars[i + 1].is_ascii_digit()
            && chars[i + 2].is_ascii_digit()
        {
            let code: String = chars[i..i + 3].iter().collect();
            if let Ok(status) = code.parse::<u16>() {
                return is_status_retryable(status);
            }
            i += 3;
            continue;
        }
        i += 1;
    }
    // 无三位数字码:按文本分类(timeout/connect/transport/decode 可重试,其余 unknown 保守不重试)
    // BUG-260617-01:Anthropic 中文参数错误(如 `[1214][messages 参数非法]`)无 HTTP 数字前缀,
    // 走此文本分支,原 || true 致 retryable=true 空耗 4 次重试浪费 token。
    // 删 || true 对齐 retry::is_status_retryable 未知码不可重试语义;timeout/connect 瞬态仍可重试。
    //
    // BUG-2026-07-17 根治:SSE 解码/传输错误(error decoding response body / Transport error)
    // 是传输层错误,不是 Fatal。原实现未识别这些关键词,返 unknown→Fatal,致连续重试 4 次均
    // 立即放弃。现补全传输层关键词,让这类错误走可重试路径。
    let lower = status_or_class.to_lowercase();
    lower.contains("timeout")
        || lower.contains("connect")
        || lower.contains("transport")
        || lower.contains("decoding")
        || lower.contains("decode")
        || lower.contains("body")
        || lower.contains("eof")
        || lower.contains("reset")
        || lower.contains("broken pipe")
}

// ============================================================
// 单测：诊断提取/格式化（纯函数，不发 HTTP、不依赖 app_handle）
// ============================================================

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

    // ---- classify_status_or_class：CR-30-1 retryable 分类(镜像 retry::is_status_retryable) ----

    /// 5xx → retryable=true
    #[test]
    fn classify_5xx_retryable() {
        assert!(classify_status_or_class("HTTP 500"));
        assert!(classify_status_or_class("HTTP 503"));
    }

    /// 429 → retryable=true(限流可重试)
    #[test]
    fn classify_429_retryable() {
        assert!(classify_status_or_class("HTTP 429"));
    }

    /// 4xx(非429) → retryable=false Fatal(鉴权/参数错立即放弃)
    #[test]
    fn classify_4xx_fatal() {
        assert!(!classify_status_or_class("HTTP 401"));
        assert!(!classify_status_or_class("HTTP 403"));
        assert!(!classify_status_or_class("HTTP 404"));
        assert!(!classify_status_or_class("HTTP 422"));
    }

    /// timeout / connect → retryable=true(瞬态)
    #[test]
    fn classify_transient_retryable() {
        assert!(classify_status_or_class("timeout"));
        assert!(classify_status_or_class("connect"));
    }

    /// unknown → 保守 retryable=false(BUG-260617-01:防 Anthropic 中文参数错误等 Fatal 空耗重试)
    #[test]
    fn classify_unknown_not_retryable() {
        assert!(!classify_status_or_class("unknown"));
    }

    // ---- extract_error_diag：业务层 bail（provider 串已含状态码）----

    /// provider 在 non-2xx bail 的典型串：抠出 401（鉴权失败/Key 错）
    #[test]
    fn diag_extracts_401_from_provider_bail() {
        let e = anyhow::anyhow!("LLM 流式 API 错误 401: Unauthorized");
        let (status, raw) = extract_error_diag(&e);
        assert_eq!(status, "HTTP 401");
        assert!(raw.contains("401"));
    }

    /// 404 = base_url/endpoint 不对
    #[test]
    fn diag_extracts_404_from_provider_bail() {
        let e = anyhow::anyhow!("LLM 流式 API 错误 404: Not Found");
        assert_eq!(extract_error_diag(&e).0, "HTTP 404");
    }

    /// 429 = 限流
    #[test]
    fn diag_extracts_429_from_provider_bail() {
        let e = anyhow::anyhow!("LLM 流式 API 错误 429: rate limit");
        assert_eq!(extract_error_diag(&e).0, "HTTP 429");
    }

    /// 5xx = 上游服务端错
    #[test]
    fn diag_extracts_500_from_provider_bail() {
        let e = anyhow::anyhow!("LLM 流式 API 错误 500: Internal Server Error");
        assert_eq!(extract_error_diag(&e).0, "HTTP 500");
    }

    // ---- extract_error_diag：传输层（reqwest Display 文本，不命名 reqwest）----

    /// 超时：连接成功但响应慢/静默断
    #[test]
    fn diag_classifies_timeout() {
        let e = anyhow::anyhow!("error sending request for url (https://api.x.com/v1/chat/completions): operation timed out");
        let (status, _) = extract_error_diag(&e);
        assert_eq!(status, "timeout");
    }

    /// 中文「超时」关键词
    #[test]
    fn diag_classifies_timeout_cn() {
        let e = anyhow::anyhow!("请求超时");
        assert_eq!(extract_error_diag(&e).0, "timeout");
    }

    /// 连接失败：DNS 解析失败 / 端点不通
    #[test]
    fn diag_classifies_connect() {
        let e = anyhow::anyhow!("dns error: failed to lookup address information");
        assert_eq!(extract_error_diag(&e).0, "connect");
    }

    /// resolve 关键词也归 connect
    #[test]
    fn diag_classifies_connect_resolve() {
        let e = anyhow::anyhow!("error connecting: resolve failed");
        assert_eq!(extract_error_diag(&e).0, "connect");
    }

    // ---- extract_error_diag：边界 ----

    /// 三位数但不在已知 HTTP 状态白名单（如 "200"）→ 不当状态码，走分类
    #[test]
    fn diag_ignores_non_http_status_number() {
        let e = anyhow::anyhow!("成功 200 条记录");
        // 200 不在白名单，应落到 unknown
        assert_eq!(extract_error_diag(&e).0, "unknown");
    }

    /// 完全无特征文本 → unknown，且 raw 原样返回
    #[test]
    fn diag_unknown_preserves_raw() {
        let e = anyhow::anyhow!("奇怪的错误 xyz");
        let (status, raw) = extract_error_diag(&e);
        assert_eq!(status, "unknown");
        assert_eq!(raw, "奇怪的错误 xyz");
    }

    /// 状态码出现在更长数字串里也不误匹配（边界判断：前后必须非数字）
    #[test]
    fn diag_does_not_match_status_inside_longer_digits() {
        // R-P2-6:边界判断后,白名单码嵌在长数字串内不再误命中(原窗口切片会从 14012 抠出 401)
        let e = anyhow::anyhow!("port 14012 used");
        assert_eq!(extract_error_diag(&e).0, "unknown");
        // 纯长数字串(无白名单码)同样不命中
        let e2 = anyhow::anyhow!("port 99999 used");
        assert_eq!(extract_error_diag(&e2).0, "unknown");
    }

    // ---- fmt_diag：两个上下文 ----

    #[test]
    fn fmt_diag_init_branch() {
        let s = fmt_diag("anthropic-compat", DiagKind::Init, "HTTP 401", "Unauthorized");
        assert_eq!(s, "[anthropic-compat] AI 调用失败(HTTP 401): Unauthorized");
    }

    /// R-P2-6:状态码紧贴中文字符(多字节 UTF-8)时仍能正确抠出
    /// (原按字节窗口切片在中文边界脆弱,改为 char 迭代后安全)
    #[test]
    fn diag_extracts_code_adjacent_to_multibyte_chars() {
        let e = anyhow::anyhow!("错误401Unauthorized");
        assert_eq!(extract_error_diag(&e).0, "HTTP 401");
        // 中文在前:状态码前字符是多字节中文,char 迭代边界判断正确
        let e2 = anyhow::anyhow!("请求失败：401");
        assert_eq!(extract_error_diag(&e2).0, "HTTP 401");
    }

    #[test]
    fn fmt_diag_midstream_branch() {
        let s = fmt_diag("gpt-4o", DiagKind::MidStream, "timeout", "operation timed out");
        assert_eq!(s, "[gpt-4o] 流式接收错误(timeout): operation timed out");
    }

    /// provider 名/错误文本含特殊字符仍按模板拼接（无格式注入风险）
    #[test]
    fn fmt_diag_handles_special_chars() {
        let s = fmt_diag("model/x", DiagKind::Init, "unknown", "err: {json} \"quoted\"");
        assert_eq!(s, "[model/x] AI 调用失败(unknown): err: {json} \"quoted\"");
    }
}
