# -*- coding: utf-8 -*-
"""
413 两阶段恢复测试（红-绿-重构中的红阶段）。

借鉴 zhikuncode-main 的 CollapseDrain + ReactiveCompact 机制：当 LLM API
返回 HTTP 413（Payload Too Large）时，调用 on_413_callback 执行两阶段
激进压缩后重试，避免长对话直接失败。

测试覆盖：
1. 413 触发 Phase 1：回调返回 True，重试成功
2. Phase 1 后成功：日志记录「413 恢复成功（Phase 1）」
3. Phase 2 后成功：Phase 1 仍 413，Phase 2 压缩后重试成功
4. 两阶段均失败：抛 RuntimeError("413 两阶段恢复失败")
5. 非 413（429/500/503）走原有重试，不调用 on_413_callback
6. 回调返回 False（压缩失败）直接抛异常
"""
from unittest.mock import MagicMock, patch

import httpx
import pytest
from openai import APIStatusError, RateLimitError, InternalServerError

from abyss.api_client import DeepSeekClient


def _make_413_error(msg: str = "Payload Too Large") -> APIStatusError:
    """构造 status_code=413 的 APIStatusError。

    APIStatusError.__init__(message, *, response, body)，
    status_code 属性由 response.status_code 设置。
    """
    response = httpx.Response(
        status_code=413,
        request=httpx.Request("POST", "https://api.deepseek.com/chat"),
    )
    return APIStatusError(msg, response=response, body=None)


def _make_chunk(content=None, reasoning=None, finish=None):
    """构造与 _parse_chunk 兼容的最小流式 chunk。"""
    delta = MagicMock()
    delta.content = content
    delta.reasoning_content = reasoning
    delta.tool_calls = None
    choice = MagicMock()
    choice.delta = delta
    choice.finish_reason = finish
    chunk = MagicMock()
    chunk.choices = [choice]
    chunk.usage = None
    return chunk


def _make_rate_limit_error() -> RateLimitError:
    """构造 429 RateLimitError。"""
    response = httpx.Response(
        status_code=429,
        request=httpx.Request("POST", "https://api.deepseek.com/chat"),
    )
    return RateLimitError("rate limited", response=response, body=None)


def _make_500_error() -> InternalServerError:
    """构造 500 InternalServerError。"""
    response = httpx.Response(
        status_code=500,
        request=httpx.Request("POST", "https://api.deepseek.com/chat"),
    )
    return InternalServerError("server error", response=response, body=None)


def _make_503_error() -> APIStatusError:
    """构造 503 APIStatusError。"""
    response = httpx.Response(
        status_code=503,
        request=httpx.Request("POST", "https://api.deepseek.com/chat"),
    )
    return APIStatusError("unavailable", response=response, body=None)


class Test413Phase1Recovery:
    """413 触发 Phase 1 压缩后重试成功。"""

    def test_413_triggers_phase1_callback_then_succeeds(self):
        """413 → 调用 on_413_callback(1) → 回调返回 True → 重试成功。"""
        client = DeepSeekClient(api_key="fake-key")
        client.client = MagicMock()
        # 第一次抛 413，第二次返回成功流
        success_stream = iter([_make_chunk(content="hi"), _make_chunk(finish="stop")])
        client.client.chat.completions.create.side_effect = [
            _make_413_error(),
            success_stream,
        ]

        callback = MagicMock(return_value=True)
        with patch("abyss.api_client.logger") as mock_logger:
            mock_logger.api_request = MagicMock()
            mock_logger.api_retry = MagicMock()
            chunks = list(client.chat(
                messages=[{"role": "user", "content": "hi"}],
                stream=True,
                on_413_callback=callback,
            ))

        # 回调被调用一次，phase=1
        callback.assert_called_once_with(1)
        # 重试成功，拿到 chunks
        assert len(chunks) == 2
        assert chunks[0]["content"] == "hi"

    def test_phase1_success_logs_recovery_message(self):
        """Phase 1 成功后应记录「413 恢复成功（Phase 1）」日志。"""
        client = DeepSeekClient(api_key="fake-key")
        client.client = MagicMock()
        success_stream = iter([_make_chunk(content="ok"), _make_chunk(finish="stop")])
        client.client.chat.completions.create.side_effect = [
            _make_413_error(),
            success_stream,
        ]

        callback = MagicMock(return_value=True)
        with patch("abyss.api_client.logger") as mock_logger:
            mock_logger.api_request = MagicMock()
            mock_logger.api_retry = MagicMock()
            list(client.chat(
                messages=[{"role": "user", "content": "hi"}],
                stream=True,
                on_413_callback=callback,
            ))
            # 验证有日志包含「413 恢复成功」和「Phase 1」
            all_calls = []
            for c in mock_logger.api_retry.call_args_list:
                all_calls.append(str(c))
            # 至少有一条日志提到 413 恢复成功 Phase 1
            joined = " | ".join(all_calls)
            # 也检查 api_error / 其他调用
            for method_name in ("api_error", "info", "warning"):
                m = getattr(mock_logger, method_name, None)
                if m is not None and m.called:
                    for c in m.call_args_list:
                        all_calls.append(str(c))
            joined = " | ".join(all_calls)
            assert "413" in joined, f"日志应包含 413 字样，实际：{joined!r}"
            assert "Phase 1" in joined or "phase 1" in joined.lower(), (
                f"日志应包含 Phase 1 字样，实际：{joined!r}"
            )


class Test413Phase2Recovery:
    """Phase 1 仍 413，Phase 2 压缩后重试成功。"""

    def test_phase2_recovery_succeeds_after_phase1_still_413(self):
        """Phase 1 后仍 413 → 调用 on_413_callback(2) → Phase 2 后重试成功。"""
        client = DeepSeekClient(api_key="fake-key")
        client.client = MagicMock()
        success_stream = iter([_make_chunk(content="ok"), _make_chunk(finish="stop")])
        client.client.chat.completions.create.side_effect = [
            _make_413_error(),  # 首次请求
            _make_413_error(),  # Phase 1 重试
            success_stream,     # Phase 2 重试成功
        ]

        callback = MagicMock(return_value=True)
        with patch("abyss.api_client.logger") as mock_logger:
            mock_logger.api_request = MagicMock()
            mock_logger.api_retry = MagicMock()
            chunks = list(client.chat(
                messages=[{"role": "user", "content": "hi"}],
                stream=True,
                on_413_callback=callback,
            ))

        # 回调依次以 phase=1 和 phase=2 被调用
        assert callback.call_args_list == [
            ((1,), {}),
            ((2,), {}),
        ], f"回调调用顺序错误：{callback.call_args_list}"
        assert len(chunks) == 2
        assert chunks[0]["content"] == "ok"


class Test413BothPhasesFail:
    """两阶段压缩后仍 413 → 抛 RuntimeError。"""

    def test_both_phases_fail_raises_runtime_error(self):
        """Phase 1 + Phase 2 后仍 413 → 抛 RuntimeError，消息含「413 两阶段恢复失败」。"""
        client = DeepSeekClient(api_key="fake-key")
        client.client = MagicMock()
        client.client.chat.completions.create.side_effect = [
            _make_413_error(),  # 首次
            _make_413_error(),  # Phase 1 重试
            _make_413_error(),  # Phase 2 重试
        ]

        callback = MagicMock(return_value=True)
        with patch("abyss.api_client.logger") as mock_logger:
            mock_logger.api_request = MagicMock()
            mock_logger.api_retry = MagicMock()
            mock_logger.api_error = MagicMock()
            with pytest.raises(RuntimeError) as exc_info:
                list(client.chat(
                    messages=[{"role": "user", "content": "hi"}],
                    stream=True,
                    on_413_callback=callback,
                ))
        # 异常消息应包含「413 两阶段恢复失败」
        assert "413" in str(exc_info.value), (
            f"异常消息应包含 413，实际：{exc_info.value!r}"
        )
        # 回调被调用两次：phase=1, phase=2
        assert callback.call_args_list == [((1,), {}), ((2,), {})]


class TestNon413NotTriggerCallback:
    """非 413 错误码不应触发 on_413_callback。"""

    def test_429_does_not_trigger_413_callback(self):
        """429 RateLimitError 走原有重试，不调用 on_413_callback。"""
        client = DeepSeekClient(api_key="fake-key")
        client.client = MagicMock()
        client.retry_delay = 0.001
        client.client.chat.completions.create.side_effect = _make_rate_limit_error()

        callback = MagicMock(return_value=True)
        with patch("abyss.api_client.logger") as mock_logger:
            mock_logger.api_request = MagicMock()
            mock_logger.api_retry = MagicMock()
            mock_logger.api_error = MagicMock()
            with pytest.raises(RateLimitError):
                list(client.chat(
                    messages=[{"role": "user", "content": "hi"}],
                    stream=False,
                    on_413_callback=callback,
                ))
        # 413 回调绝不能被调用
        callback.assert_not_called()

    def test_500_does_not_trigger_413_callback(self):
        """500 InternalServerError 走原有重试，不调用 on_413_callback。"""
        client = DeepSeekClient(api_key="fake-key")
        client.client = MagicMock()
        client.retry_delay = 0.001
        client.client.chat.completions.create.side_effect = _make_500_error()

        callback = MagicMock(return_value=True)
        with patch("abyss.api_client.logger") as mock_logger:
            mock_logger.api_request = MagicMock()
            mock_logger.api_retry = MagicMock()
            mock_logger.api_error = MagicMock()
            with pytest.raises(InternalServerError):
                list(client.chat(
                    messages=[{"role": "user", "content": "hi"}],
                    stream=False,
                    on_413_callback=callback,
                ))
        callback.assert_not_called()

    def test_503_does_not_trigger_413_callback(self):
        """503 走原有 RETRYABLE_CODES 重试，不调用 on_413_callback。"""
        client = DeepSeekClient(api_key="fake-key")
        client.client = MagicMock()
        client.retry_delay = 0.001
        client.client.chat.completions.create.side_effect = _make_503_error()

        callback = MagicMock(return_value=True)
        with patch("abyss.api_client.logger") as mock_logger:
            mock_logger.api_request = MagicMock()
            mock_logger.api_retry = MagicMock()
            mock_logger.api_error = MagicMock()
            with pytest.raises(APIStatusError):
                list(client.chat(
                    messages=[{"role": "user", "content": "hi"}],
                    stream=False,
                    on_413_callback=callback,
                ))
        callback.assert_not_called()


class TestCallbackReturnsFalse:
    """回调返回 False（压缩失败）→ 直接抛异常，不重试。"""

    def test_callback_returns_false_raises_and_no_retry(self):
        """回调返回 False → 抛 RuntimeError，不进入 Phase 2。"""
        client = DeepSeekClient(api_key="fake-key")
        client.client = MagicMock()
        client.client.chat.completions.create.side_effect = _make_413_error()

        callback = MagicMock(return_value=False)
        with patch("abyss.api_client.logger") as mock_logger:
            mock_logger.api_request = MagicMock()
            mock_logger.api_retry = MagicMock()
            mock_logger.api_error = MagicMock()
            with pytest.raises(RuntimeError):
                list(client.chat(
                    messages=[{"role": "user", "content": "hi"}],
                    stream=True,
                    on_413_callback=callback,
                ))
        # 回调只被调用一次（Phase 1 失败，不再尝试 Phase 2）
        assert callback.call_count == 1, (
            f"回调返回 False 后不应继续重试，调用次数：{callback.call_count}"
        )
