# -*- coding: utf-8 -*-
"""
回归测试：APIConnectionError 必须输出可诊断信息，而不是仅 "Connection error."。

复现：用户日志 abyss_2026-07-19.log 第 9-13 行显示，flash 模型连续 3 次
APIConnectionError 失败，但日志只记录了 "Connection error."，丢失了底层
httpx 异常类型与原因，无法判断是 DNS / TLS / 超时 / 服务端 5xx 等。

本测试覆盖：
1. retry 日志的 reason 字段必须包含底层 cause 的类型信息（不仅 "APIConnectionError"）
2. 重试耗尽后，最终 api_error 日志必须包含模型名 + cause 摘要
3. main.py 在 APIConnectionError 时给用户友好提示（建议稍后重试 / 切换模型）
"""
from unittest.mock import MagicMock, patch

import pytest
from openai import APIConnectionError

from abyss.api_client import DeepSeekClient


def _make_conn_error(cause_msg: str = "Connection refused") -> APIConnectionError:
    """构造带 __cause__ 的 APIConnectionError，模拟 OpenAI SDK 行为。

    SDK 在网络层失败时会把 httpx 异常作为 __cause__ 链上。
    """
    cause = ConnectionError(cause_msg)
    err = APIConnectionError(request=MagicMock(), message="Connection error.")
    err.__cause__ = cause
    return err


class TestApiConnectionErrorDiag:
    """APIConnectionError 诊断信息完整性。"""

    def test_retry_reason_string_contains_cause(self):
        """更直接的测试：spy logger.api_retry，验证 reason 字符串包含 cause 类型。

        红：当前 reason="APIConnectionError"，无 cause 信息。
        绿：reason 应包含 "ConnectionError"（cause 类型）。
        """
        client = DeepSeekClient(api_key="fake-key")
        client.client = MagicMock()
        client.client.chat.completions.create.side_effect = _make_conn_error("ECONNREFUSED")
        client.retry_delay = 0.001  # 极短 backoff

        # Spy logger.api_retry
        with patch("abyss.api_client.logger") as mock_logger:
            mock_logger.api_retry = MagicMock()
            mock_logger.api_request = MagicMock()
            with pytest.raises(APIConnectionError):
                list(client.chat(
                    messages=[{"role": "user", "content": "hi"}],
                    model="deepseek-v4-flash",
                    stream=False,
                ))

            # 验证 api_retry 被调用
            assert mock_logger.api_retry.called, "api_retry 应至少被调用一次"
            calls = mock_logger.api_retry.call_args_list
            # 至少 2 次 retry
            assert len(calls) >= 2, f"应有 ≥2 次 retry，实际 {len(calls)} 次"

            # 每次 retry 的 reason 参数（第 3 个位置参数）应包含 cause 类型
            for call in calls:
                # api_retry(attempt, wait_s, reason) — reason 是第 3 个位置参数
                args, kwargs = call
                if len(args) >= 3:
                    reason = args[2]
                else:
                    reason = kwargs.get("reason", "")
                # reason 不能仅是 "APIConnectionError"，应包含 cause 信息
                assert reason != "APIConnectionError", (
                    f"reason 不能仅是 'APIConnectionError'，应包含 cause 类型。"
                    f"实际：{reason!r}"
                )
                # 应包含 "ConnectionError"（cause 类型名）
                assert "ConnectionError" in reason, (
                    f"reason 应包含 cause 类型名 'ConnectionError'，实际：{reason!r}"
                )

    def test_final_api_error_log_includes_model_and_cause(self):
        """重试耗尽后，最终 api_error 日志必须包含模型名 + cause 摘要。

        红：当前 chat() 只在 raise 后由 main.py 记一条 api_error(str(e))，
           即 "Connection error."，丢失模型与 cause 信息。
        绿：chat() 在重试耗尽时应额外记录一条带 model + cause 的诊断日志。
        """
        client = DeepSeekClient(api_key="fake-key")
        client.client = MagicMock()
        client.client.chat.completions.create.side_effect = _make_conn_error("tls handshake failed")
        client.retry_delay = 0.001

        with patch("abyss.api_client.logger") as mock_logger:
            mock_logger.api_retry = MagicMock()
            mock_logger.api_request = MagicMock()
            mock_logger.api_error = MagicMock()
            with pytest.raises(APIConnectionError):
                list(client.chat(
                    messages=[{"role": "user", "content": "hi"}],
                    model="deepseek-v4-flash",
                    stream=False,
                ))

            # api_error 应被调用，且消息包含模型名
            assert mock_logger.api_error.called, (
                "重试耗尽后应调用 api_error 记录最终失败，但未调用"
            )
            args, kwargs = mock_logger.api_error.call_args
            error_msg = args[0] if args else kwargs.get("error", "")
            assert "deepseek-v4-flash" in error_msg, (
                f"最终 api_error 日志应包含模型名，实际：{error_msg!r}"
            )
            assert "tls handshake failed" in error_msg or "ConnectionError" in error_msg, (
                f"最终 api_error 日志应包含 cause 摘要，实际：{error_msg!r}"
            )

    def test_main_loop_friendly_message_for_api_connection_error(self):
        """main.py 主循环捕获 APIConnectionError 时，给用户友好提示。

        红：当前显示 "API 调用失败: Connection error."，无行动指引。
        绿：应显示包含"重试"/"切换模型"/"网络"等关键词的友好提示。
        """
        from abyss import main as main_mod

        # 检查源码中是否有针对 APIConnectionError 的特殊处理分支
        import inspect
        src = inspect.getsource(main_mod)
        # 应包含 APIConnectionError 的处理分支
        assert "APIConnectionError" in src, (
            "main.py 应针对 APIConnectionError 做特殊处理（友好提示），"
            "但代码中未引用该异常类型"
        )
