//! 金标测试组 — LLM 通道错误分类防御
//!
//! 这是 CI 准入的核心防御测试。每条断言都是对一个历史事故的锁定：
//!
//! - `http_402_classified_as_quota` — 锁定 402 失效事故（commit 50099b2 修复）：
//!   HttpStatus(402) 必须归类正确，is_quota() 必须 true
//! - `http_429_classified_as_rate_limit` — 速率限制必须明确分类
//! - `http_500_classified_as_server_error` — 服务端错误分类
//! - `sse_done_with_zero_chunks_is_error` — [DONE] 但无 content 必须报错
//! - `sse_success_returns_content` — 基本 happy path
//! - `request_sends_correct_format` — 验证发出的 HTTP 请求符合 OpenAI 协议
//!
//! 任何修改 parse_channel_error / SSE 流处理 / InferErrorCode 分类逻辑的改动，
//! 都必须保证这组测试全绿 — 否则视为回归。

mod common;

use common::mock_llm_server::{MockLlmServer, Script, SseUsageSpec};

#[allow(unused_imports)]
use mad_hatter::{InferErrorCode, LlmChannel, OpenAiChannel, StreamChunk};

/// 辅助：把 channel 返回的 StreamChunk 收集成 Vec（直到 Eof 或通道关闭）
async fn drain_stream(
    mut rx: tokio::sync::mpsc::UnboundedReceiver<StreamChunk>,
) -> Vec<StreamChunk> {
    let mut out = Vec::new();
    while let Some(chunk) = rx.recv().await {
        let is_eof = matches!(chunk, StreamChunk::Eof { .. });
        out.push(chunk);
        if is_eof {
            break;
        }
    }
    out
}

// ============================================================
// HTTP 状态码分类 — 核心防御组
// ============================================================

#[tokio::test]
async fn http_402_classified_as_quota() {
    let server = MockLlmServer::start().await;
    server.push_script(Script::quota_exceeded()).await;

    let channel = OpenAiChannel::new(server.base_url(), "test-model", "fake-key");
    let rx = channel
        .start_stream("hello".to_string())
        .expect("start_stream should return Ok");
    let chunks = drain_stream(rx).await;

    // 期望收到 Error chunk，内容以 "HTTP 402:" 开头
    let err_chunk = chunks
        .iter()
        .find_map(|c| match c {
            StreamChunk::Error(s) => Some(s.as_str()),
            _ => None,
        })
        .expect("should receive StreamChunk::Error for 402");

    assert!(
        err_chunk.starts_with("HTTP 402:"),
        "error message must start with 'HTTP 402:' to enable classification, got: {:?}",
        err_chunk
    );

    // 这是 402 事故的直接防御：确保 HTTP 402 能被 InferErrorCode::HttpStatus(402) 捕获
    // 从而 is_quota() 返回 true —— 否则 Alice 引擎的余额不足特殊处理会失效
    // 此处我们验证"消息格式"——分类由 parse_channel_error 完成，它看的就是这个前缀
}

#[tokio::test]
async fn http_429_classified_as_rate_limit() {
    let server = MockLlmServer::start().await;
    server.push_script(Script::rate_limited()).await;

    let channel = OpenAiChannel::new(server.base_url(), "test-model", "fake-key");
    let rx = channel.start_stream("hi".to_string()).expect("start_stream");
    let chunks = drain_stream(rx).await;

    let err = chunks
        .iter()
        .find_map(|c| match c {
            StreamChunk::Error(s) => Some(s.as_str()),
            _ => None,
        })
        .expect("should receive Error for 429");
    assert!(err.starts_with("HTTP 429:"), "got: {:?}", err);
}

#[tokio::test]
async fn http_500_classified_as_server_error() {
    let server = MockLlmServer::start().await;
    server.push_script(Script::server_error()).await;

    let channel = OpenAiChannel::new(server.base_url(), "test-model", "fake-key");
    let rx = channel.start_stream("hi".to_string()).expect("start_stream");
    let chunks = drain_stream(rx).await;

    let err = chunks
        .iter()
        .find_map(|c| match c {
            StreamChunk::Error(s) => Some(s.as_str()),
            _ => None,
        })
        .expect("should receive Error for 500");
    assert!(err.starts_with("HTTP 500:"), "got: {:?}", err);
}

// ============================================================
// SSE 异常 — sanity guards
// ============================================================

#[tokio::test]
async fn sse_done_with_zero_chunks_is_error() {
    let server = MockLlmServer::start().await;
    server.push_script(Script::SseDoneNoChunks).await;

    let channel = OpenAiChannel::new(server.base_url(), "test-model", "fake-key");
    let rx = channel.start_stream("hi".to_string()).expect("start_stream");
    let chunks = drain_stream(rx).await;

    // 协议走完但 content 为空 — 必须报错（防 "中毒 prompt" 场景沉默）
    let err = chunks.iter().find_map(|c| match c {
        StreamChunk::Error(s) => Some(s.as_str()),
        _ => None,
    });
    assert!(
        err.is_some(),
        "SSE [DONE] with 0 content chunks must surface an Error, chunks: {:?}",
        chunks
    );
    let err = err.unwrap();
    assert!(
        err.contains("0 content chunks"),
        "error message should mention '0 content chunks', got: {:?}",
        err
    );
}

#[tokio::test]
async fn sse_success_returns_content() {
    let server = MockLlmServer::start().await;
    server
        .push_script(Script::sse_streamed("hello world", 3))
        .await;

    let channel = OpenAiChannel::new(server.base_url(), "test-model", "fake-key");
    let rx = channel.start_stream("hi".to_string()).expect("start_stream");
    let chunks = drain_stream(rx).await;

    // 拼接所有 Content chunk，应该还原原文
    let text: String = chunks
        .iter()
        .filter_map(|c| match c {
            StreamChunk::Content(s) => Some(s.as_str()),
            _ => None,
        })
        .collect();
    assert_eq!(text, "hello world", "full chunks: {:?}", chunks);

    // 应该收到 Eof { clean: true }
    let eof = chunks.iter().find_map(|c| match c {
        StreamChunk::Eof { clean } => Some(*clean),
        _ => None,
    });
    assert_eq!(eof, Some(true), "expected clean EOF, got: {:?}", chunks);

    // 不应收到 Error
    let err = chunks.iter().find(|c| matches!(c, StreamChunk::Error(_)));
    assert!(err.is_none(), "unexpected error chunk: {:?}", err);
}

#[tokio::test]
async fn sse_usage_captured() {
    let server = MockLlmServer::start().await;
    server
        .push_script(Script::SseFull {
            content_chunks: vec!["ok".to_string()],
            usage: Some(SseUsageSpec {
                prompt_tokens: 100,
                completion_tokens: 20,
                cached_tokens: 60,
            }),
            finish_reason: "stop".to_string(),
        })
        .await;

    let channel = OpenAiChannel::new(server.base_url(), "test-model", "fake-key");
    let rx = channel.start_stream("hi".to_string()).expect("start_stream");
    let chunks = drain_stream(rx).await;

    // 应至少收到一次 Usage
    let usage = chunks
        .iter()
        .find_map(|c| match c {
            StreamChunk::Usage(u) => Some(u),
            _ => None,
        })
        .expect("should capture usage");
    assert_eq!(usage.prompt_tokens, 100);
    assert_eq!(usage.completion_tokens, 20);
}

// ============================================================
// 请求格式验证 — OpenAI 协议合规
// ============================================================

#[tokio::test]
async fn request_sends_correct_format() {
    let server = MockLlmServer::start().await;
    server
        .push_script(Script::sse_content_only("done"))
        .await;

    let channel = OpenAiChannel::new(server.base_url(), "gpt-test", "sk-abc123");
    let rx = channel
        .start_stream("what is 1+1?".to_string())
        .expect("start_stream");
    let _ = drain_stream(rx).await;

    let reqs = server.requests().await;
    assert_eq!(reqs.len(), 1, "should have 1 request");
    let req = &reqs[0];

    assert_eq!(req.method, "POST");
    assert_eq!(req.path, "/v1/chat/completions");
    assert_eq!(
        req.authorization.as_deref(),
        Some("Bearer sk-abc123"),
        "Authorization header must be Bearer token"
    );

    // 解析 JSON body 验证关键字段
    let body_json: serde_json::Value =
        serde_json::from_str(&req.body).expect("body should be valid JSON");
    assert_eq!(body_json["model"], "gpt-test");
    assert_eq!(body_json["stream"], true);
    assert_eq!(
        body_json["stream_options"]["include_usage"], true,
        "must request usage info"
    );
    assert_eq!(body_json["messages"][0]["role"], "user");
    assert_eq!(body_json["messages"][0]["content"], "what is 1+1?");
}

// ============================================================
// 多次请求 — 脚本队列顺序消费
// ============================================================

#[tokio::test]
async fn scripts_consumed_in_fifo_order() {
    let server = MockLlmServer::start().await;
    server
        .push_scripts(vec![
            Script::quota_exceeded(),
            Script::sse_content_only("retry ok"),
        ])
        .await;

    let channel = OpenAiChannel::new(server.base_url(), "m", "k");

    // 第一次：402
    let rx = channel.start_stream("t1".to_string()).unwrap();
    let c1 = drain_stream(rx).await;
    let has_402 = c1.iter().any(|c| match c {
        StreamChunk::Error(s) => s.starts_with("HTTP 402:"),
        _ => false,
    });
    assert!(has_402, "first request should hit 402");

    // 第二次：成功
    let rx = channel.start_stream("t2".to_string()).unwrap();
    let c2 = drain_stream(rx).await;
    let text: String = c2
        .iter()
        .filter_map(|c| match c {
            StreamChunk::Content(s) => Some(s.as_str()),
            _ => None,
        })
        .collect();
    assert_eq!(text, "retry ok");

    assert_eq!(server.request_count().await, 2);
}