"""AI Client (DoubaoClient) 单元测试"""

from __future__ import annotations

from unittest.mock import MagicMock, patch

import pytest

from packages.shared.ai_client import DoubaoClient, get_doubao_client


@pytest.fixture
def mock_settings():
    """模拟配置"""
    with patch("packages.shared.ai_client.get_shared_settings") as mock:
        mock.return_value = MagicMock(
            doubao_api_key="test-api-key",
            doubao_model="doubao-pro-32k",
            doubao_base_url="https://ark.example.com/api/v3",
            doubao_timeout=30,
            doubao_max_retries=2,
        )
        yield mock


@pytest.fixture
def client_with_key(mock_settings):
    """有 API Key 的客户端"""
    return DoubaoClient()


@pytest.fixture
def client_without_key():
    """没有 API Key 的客户端"""
    with patch("packages.shared.ai_client.get_shared_settings") as mock:
        mock.return_value = MagicMock(
            doubao_api_key="",
            doubao_model="doubao-pro-32k",
            doubao_base_url="https://ark.example.com/api/v3",
            doubao_timeout=30,
            doubao_max_retries=2,
        )
        yield DoubaoClient()


class TestDoubaoClientInit:
    """初始化测试"""

    def test_init_with_api_key(self, mock_settings):
        """有 API Key 时初始化正常"""
        client = DoubaoClient()
        assert client.api_key == "test-api-key"
        assert client.model == "doubao-pro-32k"
        assert client.base_url == "https://ark.example.com/api/v3"
        assert client.timeout == 30
        assert client.max_retries == 2

    def test_base_url_strips_trailing_slash(self, mock_settings):
        """base_url 去掉末尾斜杠"""
        mock_settings.return_value.doubao_base_url = "https://ark.example.com/api/v3/"
        client = DoubaoClient()
        assert client.base_url == "https://ark.example.com/api/v3"


class TestIsAvailable:
    """is_available 属性测试"""

    def test_available_with_key(self, client_with_key):
        """有 API Key 时可用"""
        assert client_with_key.is_available is True

    def test_unavailable_without_key(self, client_without_key):
        """无 API Key 时不可用"""
        assert client_without_key.is_available is False


class TestChatCompletion:
    """chat_completion 方法测试"""

    def test_success_returns_content(self, client_with_key):
        """成功调用返回内容"""
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {"choices": [{"message": {"content": "  你好，我是豆包  "}}]}
        mock_response.raise_for_status = MagicMock()

        with patch("packages.shared.ai_client.httpx.post", return_value=mock_response) as mock_post:
            result = client_with_key.chat_completion(messages=[{"role": "user", "content": "你好"}])

            assert result == "你好，我是豆包"
            mock_post.assert_called_once()
            # 验证 URL
            call_args = mock_post.call_args
            assert call_args[0][0].endswith("/chat/completions")
            # 验证 header 包含 Authorization
            assert "Authorization" in call_args[1]["headers"]
            assert "Bearer test-api-key" in call_args[1]["headers"]["Authorization"]

    def test_unavailable_returns_none(self, client_without_key):
        """不可用时返回 None"""
        with patch("packages.shared.ai_client.httpx.post") as mock_post:
            result = client_without_key.chat_completion(messages=[{"role": "user", "content": "hi"}])
            assert result is None
            mock_post.assert_not_called()

    def test_with_temperature_and_max_tokens(self, client_with_key):
        """自定义 temperature 和 max_tokens"""
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {"choices": [{"message": {"content": "hi"}}]}
        mock_response.raise_for_status = MagicMock()

        with patch("packages.shared.ai_client.httpx.post", return_value=mock_response) as mock_post:
            client_with_key.chat_completion(
                messages=[{"role": "user", "content": "hi"}],
                temperature=0.3,
                max_tokens=512,
            )

            payload = mock_post.call_args[1]["json"]
            assert payload["temperature"] == 0.3
            assert payload["max_tokens"] == 512

    def test_retry_on_failure(self, client_with_key):
        """失败时自动重试"""
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {"choices": [{"message": {"content": "success"}}]}
        mock_response.raise_for_status = MagicMock()

        call_count = 0

        def side_effect(*args, **kwargs):
            nonlocal call_count
            call_count += 1
            if call_count < 3:  # 前两次失败，第三次成功
                raise Exception("temporary error")
            return mock_response

        with patch("packages.shared.ai_client.httpx.post", side_effect=side_effect):
            with patch("packages.shared.ai_client.time.sleep"):  # 跳过 sleep
                result = client_with_key.chat_completion(messages=[{"role": "user", "content": "hi"}])
                assert result == "success"
                assert call_count == 3  # 初始 1 次 + 2 次重试

    def test_all_retries_fail_returns_none(self, client_with_key):
        """所有重试都失败返回 None"""
        with patch("packages.shared.ai_client.httpx.post", side_effect=Exception("API down")):
            with patch("packages.shared.ai_client.time.sleep"):
                result = client_with_key.chat_completion(messages=[{"role": "user", "content": "hi"}])
                assert result is None

    def test_empty_choices_returns_none(self, client_with_key):
        """空 choices 返回 None 或抛异常"""
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {"choices": []}
        mock_response.raise_for_status = MagicMock()

        with patch("packages.shared.ai_client.httpx.post", return_value=mock_response):
            with patch("packages.shared.ai_client.time.sleep"):
                # 会因 IndexError 进入异常分支，最终返回 None
                result = client_with_key.chat_completion(messages=[{"role": "user", "content": "hi"}])
                assert result is None

    def test_messages_in_payload(self, client_with_key):
        """messages 正确传递到 payload"""
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {"choices": [{"message": {"content": "ok"}}]}
        mock_response.raise_for_status = MagicMock()

        messages = [
            {"role": "system", "content": "你是助手"},
            {"role": "user", "content": "你好"},
        ]

        with patch("packages.shared.ai_client.httpx.post", return_value=mock_response) as mock_post:
            client_with_key.chat_completion(messages=messages)

            payload = mock_post.call_args[1]["json"]
            assert payload["messages"] == messages
            assert payload["model"] == "doubao-pro-32k"


class TestGetDoubaoClient:
    """单例函数测试"""

    def test_returns_same_instance(self):
        """两次调用返回同一实例"""
        client1 = get_doubao_client()
        client2 = get_doubao_client()
        assert client1 is client2

    def test_returns_doubao_client_instance(self):
        """返回 DoubaoClient 实例"""
        client = get_doubao_client()
        assert isinstance(client, DoubaoClient)
