"""
测试模型 API 调用的重试机制和错误信息增强
"""
import os
import sys
import asyncio
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch

sys.path.insert(0, str(Path(__file__).parent.parent))

import pytest

DUMMY_KEY = "sk-test-dummy-key"


@pytest.fixture
def mock_agent():
    from aacode.core.main_agent import MainAgent

    agent = MainAgent.__new__(MainAgent)
    agent.skills_manager = MagicMock()
    agent.skills_manager.list_enabled_skills = MagicMock(return_value=[])
    agent.session_manager = MagicMock()
    agent.context_manager = MagicMock()
    return agent


class TestIsRetryableModelError:
    def test_httpx_errors_are_retryable(self, mock_agent):
        import httpx
        for exc_cls in (
            httpx.RemoteProtocolError,
            httpx.ReadError,
            httpx.ConnectError,
            httpx.TimeoutException,
        ):
            try:
                exc = exc_cls("test")
            except TypeError:
                exc = exc_cls("test", request=MagicMock())
            assert mock_agent._is_retryable_model_error(exc), (
                f"{exc_cls.__name__} should be retryable"
            )

    def test_openai_retryable_errors(self, mock_agent):
        import openai
        mock_req = MagicMock()
        mock_resp = MagicMock()
        mock_resp.status_code = 502
        mock_resp.request = mock_req

        exc = openai.APIConnectionError(message="test", request=mock_req)
        assert mock_agent._is_retryable_model_error(exc)

        exc = openai.InternalServerError("test", response=mock_resp, body={})
        assert mock_agent._is_retryable_model_error(exc)

        exc = openai.RateLimitError("test", response=mock_resp, body={})
        assert mock_agent._is_retryable_model_error(exc)

    def test_openai_non_retryable_errors(self, mock_agent):
        import openai
        mock_req = MagicMock()
        mock_resp = MagicMock()
        mock_resp.status_code = 401
        mock_resp.request = mock_req

        exc = openai.AuthenticationError("test", response=mock_resp, body={})
        assert not mock_agent._is_retryable_model_error(exc)

        exc = openai.BadRequestError("test", response=mock_resp, body={})
        assert not mock_agent._is_retryable_model_error(exc)

    def test_api_status_error_5xx_is_retryable(self, mock_agent):
        import openai
        mock_resp = MagicMock()
        mock_resp.status_code = 503
        mock_resp.request = MagicMock()
        exc = openai.APIStatusError("error", response=mock_resp, body={})
        assert mock_agent._is_retryable_model_error(exc)

    def test_api_status_error_4xx_not_retryable(self, mock_agent):
        import openai
        mock_resp = MagicMock()
        mock_resp.status_code = 400
        mock_resp.request = MagicMock()
        exc = openai.APIStatusError("error", response=mock_resp, body={})
        assert not mock_agent._is_retryable_model_error(exc)

    def test_runtime_error_is_not_retryable(self, mock_agent):
        exc = RuntimeError("some error")
        assert not mock_agent._is_retryable_model_error(exc)

    def test_empty_exception_message(self, mock_agent):
        import httpx
        try:
            exc = httpx.RemoteProtocolError("", request=MagicMock())
        except TypeError:
            exc = httpx.RemoteProtocolError("")
        assert mock_agent._is_retryable_model_error(exc)


class TestModelCallerRetry:
    @pytest.mark.asyncio
    async def test_no_retry_on_success(self, mock_agent):
        mock_agent._call_openai_api = AsyncMock(return_value={"text": "ok"})

        model_caller = mock_agent._create_model_caller({"api_key": DUMMY_KEY})
        result = await model_caller([{"role": "user", "content": "hi"}])

        assert result == {"text": "ok"}
        assert mock_agent._call_openai_api.call_count == 1

    @pytest.mark.asyncio
    async def test_no_retry_on_non_retryable_error(self, mock_agent):
        import openai
        mock_resp = MagicMock()
        mock_resp.status_code = 401
        mock_resp.request = MagicMock()
        mock_agent._call_openai_api = AsyncMock(
            side_effect=openai.AuthenticationError("bad key", response=mock_resp, body={})
        )

        model_caller = mock_agent._create_model_caller({"api_key": DUMMY_KEY})
        with pytest.raises(RuntimeError):
            await model_caller([{"role": "user", "content": "hi"}])

        assert mock_agent._call_openai_api.call_count == 1

    @pytest.mark.asyncio
    async def test_retry_on_retryable_error(self, mock_agent):
        import httpx
        mock_req = MagicMock()
        mock_agent._call_openai_api = AsyncMock(
            side_effect=httpx.RemoteProtocolError("conn reset", request=mock_req)
        )

        model_caller = mock_agent._create_model_caller({"api_key": DUMMY_KEY})
        with pytest.raises(RuntimeError):
            await model_caller([{"role": "user", "content": "hi"}])

        assert mock_agent._call_openai_api.call_count == 3

    @pytest.mark.asyncio
    async def test_retry_then_succeed(self, mock_agent):
        import httpx
        mock_req = MagicMock()
        call_count = 0

        async def flaky_api(*args, **kwargs):
            nonlocal call_count
            call_count += 1
            if call_count < 2:
                raise httpx.RemoteProtocolError("conn reset", request=mock_req)
            return {"text": "ok after retry"}

        mock_agent._call_openai_api = flaky_api

        model_caller = mock_agent._create_model_caller({"api_key": DUMMY_KEY})
        result = await model_caller([{"role": "user", "content": "hi"}])

        assert result == {"text": "ok after retry"}
        assert call_count == 2


class TestErrorMessageFormat:
    @pytest.mark.asyncio
    async def test_error_includes_type_name(self, mock_agent):
        import openai
        mock_resp = MagicMock()
        mock_resp.status_code = 400
        mock_resp.request = MagicMock()
        mock_agent._call_openai_api = AsyncMock(
            side_effect=openai.BadRequestError("bad params", response=mock_resp, body={})
        )

        model_caller = mock_agent._create_model_caller({"api_key": DUMMY_KEY})
        with pytest.raises(RuntimeError) as exc_info:
            await model_caller([{"role": "user", "content": "hi"}])

        msg = str(exc_info.value)
        assert "BadRequestError" in msg, (
            f"Error message should contain exception type name, got: {msg}"
        )

    @pytest.mark.asyncio
    async def test_empty_message_still_has_type_name(self, mock_agent):
        import httpx
        try:
            exc = httpx.RemoteProtocolError("", request=MagicMock())
        except TypeError:
            exc = httpx.RemoteProtocolError("")
        mock_agent._call_openai_api = AsyncMock(side_effect=exc)

        model_caller = mock_agent._create_model_caller({"api_key": DUMMY_KEY})
        with pytest.raises(RuntimeError) as exc_info:
            await model_caller([{"role": "user", "content": "hi"}])

        msg = str(exc_info.value)
        assert "RemoteProtocolError" in msg, (
            f"Error message should contain exception type name even when str(e) is empty, got: {msg}"
        )


class TestCancelledErrorNotCaught:
    @pytest.mark.asyncio
    async def test_cancelled_error_raises_immediately(self, mock_agent):
        mock_agent._call_openai_api = AsyncMock(side_effect=asyncio.CancelledError())

        model_caller = mock_agent._create_model_caller({"api_key": DUMMY_KEY})
        with pytest.raises(asyncio.CancelledError):
            await model_caller([{"role": "user", "content": "hi"}])
