# =============================================================================
# 企微IT智能服务台 — Wingman API 端点测试
# =============================================================================
# 测试覆盖：
#   1. POST /api/conversations/{id}/wingman/draft — 正常/认证/404/降级
#   2. POST /api/conversations/{id}/wingman/summary — 正常/认证/404/降级
#   3. POST /api/conversations/{id}/wingman/tags — 正常/认证/404/降级
# =============================================================================

import uuid
from datetime import datetime
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
import pytest_asyncio
from httpx import ASGITransport, AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession

from app.models.agent import Agent
from app.models.conversation import Conversation
from app.models.message import Message
from app.services.wingman_service import WingmanService
from app.dependencies import dep_wingman_service
from app.database import get_db
from tests.conftest import create_test_conversation, create_test_agent, MockRedis


# =============================================================================
# Fixtures
# =============================================================================


@pytest_asyncio.fixture
async def wingman_client(db_session: AsyncSession, mock_redis: MockRedis):
    """提供配置了 Wingman 路由和 mock 服务的 FastAPI 测试客户端。"""

    # 创建 mock WingmanService
    mock_wingman = MagicMock(spec=WingmanService)
    mock_wingman.close = AsyncMock()

    async def _override_get_db():
        yield db_session

    async def _override_dep_wingman():
        return mock_wingman

    from app.main import create_app

    app = create_app()

    # 覆盖数据库依赖
    app.dependency_overrides[get_db] = _override_get_db
    # 覆盖 Wingman 服务依赖
    app.dependency_overrides[dep_wingman_service] = _override_dep_wingman

    # 模拟 Redis（认证依赖需要）
    # 注意：h5.py 已重构移除 _get_redis，不再需要 patch 它
    with patch("app.api.agents._get_redis", return_value=mock_redis):
        with patch("redis.asyncio.from_url", return_value=mock_redis):
            transport = ASGITransport(app=app)
            async with AsyncClient(
                transport=transport, base_url="http://test"
            ) as ac:
                # 将 mock_wingman 附加到 client 上，方便测试中配置行为
                ac._mock_wingman = mock_wingman
                yield ac

    app.dependency_overrides.clear()


@pytest_asyncio.fixture
async def authed_agent(db_session: AsyncSession, mock_redis: MockRedis):
    """创建一个已登录的坐席并返回（agent, token）。"""
    agent = create_test_agent(user_id="wingman_test_agent", name="测试坐席")
    db_session.add(agent)
    await db_session.flush()

    # 在 mock Redis 中存储 token
    token = "test-wingman-token-001"
    await mock_redis.setex(
        f"agent:token:{token}", 28800, "wingman_test_agent"
    )

    return agent, token


@pytest_asyncio.fixture
async def test_conversation(db_session: AsyncSession, authed_agent):
    """创建一个测试会话并返回。"""
    agent, _ = authed_agent
    conv = create_test_conversation(
        status="serving",
    )
    conv.assigned_agent_id = agent.user_id
    db_session.add(conv)
    await db_session.flush()

    # 添加一些消息
    msg1 = Message(
        conversation_id=conv.id,
        sender_type="employee",
        sender_id="emp001",
        sender_name="员工张三",
        content="VPN连不上怎么办",
        msg_type="text",
    )
    msg2 = Message(
        conversation_id=conv.id,
        sender_type="agent",
        sender_id=agent.user_id,
        sender_name=agent.name,
        content="请问报什么错误",
        msg_type="text",
    )
    db_session.add_all([msg1, msg2])
    await db_session.flush()

    return conv


# =============================================================================
# Draft API 测试
# =============================================================================
class TestDraftAPI:
    """测试 POST /api/conversations/{id}/wingman/draft 端点。"""

    @pytest.mark.asyncio
    async def test_draft_success(
        self, wingman_client, authed_agent, test_conversation
    ):
        """正常路径：已认证坐席为存在的会话生成草稿。"""
        agent, token = authed_agent
        conv = test_conversation
        mock_wingman = wingman_client._mock_wingman

        # 配置 mock WingmanService
        mock_wingman.generate_draft = AsyncMock(
            return_value={
                "content": "请尝试重启VPN客户端",
                "confidence": 0.85,
                "reasoning": "基于最近 2 条对话上下文生成",
            }
        )

        response = await wingman_client.post(
            f"/conversations/{conv.id}/wingman/draft",
            headers={"Authorization": f"Bearer {token}"},
        )

        assert response.status_code == 200
        data = response.json()
        assert data["code"] == 0
        assert data["data"]["content"] == "请尝试重启VPN客户端"
        assert data["data"]["confidence"] == 0.85

    @pytest.mark.asyncio
    async def test_draft_unauthorized(self, wingman_client, test_conversation):
        """认证验证：未登录坐席访问应返回错误码 1002。"""
        conv = test_conversation

        response = await wingman_client.post(
            f"/conversations/{conv.id}/wingman/draft",
        )

        data = response.json()
        assert data["code"] == 1002  # ERR_UNAUTHORIZED

    @pytest.mark.asyncio
    async def test_draft_conversation_not_found(
        self, wingman_client, authed_agent
    ):
        """会话不存在：传入不存在的 conversation_id 应返回错误码 1003。"""
        _, token = authed_agent
        fake_id = str(uuid.uuid4())

        response = await wingman_client.post(
            f"/conversations/{fake_id}/wingman/draft",
            headers={"Authorization": f"Bearer {token}"},
        )

        data = response.json()
        assert data["code"] == 1003  # ERR_NOT_FOUND

    @pytest.mark.asyncio
    async def test_draft_wingman_degradation(
        self, wingman_client, authed_agent, test_conversation
    ):
        """Wingman 降级：WingmanService 返回降级结果时，API 不应 500。"""
        agent, token = authed_agent
        conv = test_conversation
        mock_wingman = wingman_client._mock_wingman

        # 配置 mock 返回降级结果
        mock_wingman.generate_draft = AsyncMock(
            return_value={
                "content": "",
                "confidence": 0.0,
                "reasoning": "Wingman 服务暂不可用",
            }
        )

        response = await wingman_client.post(
            f"/conversations/{conv.id}/wingman/draft",
            headers={"Authorization": f"Bearer {token}"},
        )

        assert response.status_code == 200
        data = response.json()
        assert data["code"] == 0
        assert data["data"]["content"] == ""
        assert data["data"]["confidence"] == 0.0


# =============================================================================
# Summary API 测试
# =============================================================================
class TestSummaryAPI:
    """测试 POST /api/conversations/{id}/wingman/summary 端点。"""

    @pytest.mark.asyncio
    async def test_summary_success(
        self, wingman_client, authed_agent, test_conversation
    ):
        """正常路径：已认证坐席为存在的会话生成摘要。"""
        agent, token = authed_agent
        conv = test_conversation
        mock_wingman = wingman_client._mock_wingman

        mock_wingman.generate_summary = AsyncMock(
            return_value={
                "problem": "VPN连接失败",
                "cause": "证书过期",
                "solution": "更新VPN证书并重启客户端",
            }
        )

        response = await wingman_client.post(
            f"/conversations/{conv.id}/wingman/summary",
            headers={"Authorization": f"Bearer {token}"},
        )

        assert response.status_code == 200
        data = response.json()
        assert data["code"] == 0
        assert data["data"]["problem"] == "VPN连接失败"
        assert data["data"]["cause"] == "证书过期"

    @pytest.mark.asyncio
    async def test_summary_unauthorized(self, wingman_client, test_conversation):
        """认证验证：未登录坐席访问应返回错误码 1002。"""
        conv = test_conversation

        response = await wingman_client.post(
            f"/conversations/{conv.id}/wingman/summary",
        )

        data = response.json()
        assert data["code"] == 1002

    @pytest.mark.asyncio
    async def test_summary_conversation_not_found(
        self, wingman_client, authed_agent
    ):
        """会话不存在：传入不存在的 conversation_id 应返回错误码 1003。"""
        _, token = authed_agent
        fake_id = str(uuid.uuid4())

        response = await wingman_client.post(
            f"/conversations/{fake_id}/wingman/summary",
            headers={"Authorization": f"Bearer {token}"},
        )

        data = response.json()
        assert data["code"] == 1003

    @pytest.mark.asyncio
    async def test_summary_wingman_degradation(
        self, wingman_client, authed_agent, test_conversation
    ):
        """Wingman 降级：WingmanService 返回降级摘要时，API 不应 500。"""
        agent, token = authed_agent
        conv = test_conversation
        mock_wingman = wingman_client._mock_wingman

        mock_wingman.generate_summary = AsyncMock(
            return_value={
                "problem": "无法自动生成摘要",
                "cause": "",
                "solution": "",
            }
        )

        response = await wingman_client.post(
            f"/conversations/{conv.id}/wingman/summary",
            headers={"Authorization": f"Bearer {token}"},
        )

        assert response.status_code == 200
        data = response.json()
        assert data["code"] == 0
        assert data["data"]["problem"] == "无法自动生成摘要"


# =============================================================================
# Tags API 测试
# =============================================================================
class TestTagsAPI:
    """测试 POST /api/conversations/{id}/wingman/tags 端点。"""

    @pytest.mark.asyncio
    async def test_tags_success(
        self, wingman_client, authed_agent, test_conversation
    ):
        """正常路径：已认证坐席为存在的会话生成标签建议。"""
        agent, token = authed_agent
        conv = test_conversation
        mock_wingman = wingman_client._mock_wingman

        mock_wingman.suggest_tags = AsyncMock(
            return_value={
                "suggested_tags": ["VPN", "网络"],
                "category": "网络",
                "priority": "high",
            }
        )

        response = await wingman_client.post(
            f"/conversations/{conv.id}/wingman/tags",
            headers={"Authorization": f"Bearer {token}"},
        )

        assert response.status_code == 200
        data = response.json()
        assert data["code"] == 0
        assert data["data"]["suggested_tags"] == ["VPN", "网络"]
        assert data["data"]["priority"] == "high"

    @pytest.mark.asyncio
    async def test_tags_unauthorized(self, wingman_client, test_conversation):
        """认证验证：未登录坐席访问应返回错误码 1002。"""
        conv = test_conversation

        response = await wingman_client.post(
            f"/conversations/{conv.id}/wingman/tags",
        )

        data = response.json()
        assert data["code"] == 1002

    @pytest.mark.asyncio
    async def test_tags_conversation_not_found(
        self, wingman_client, authed_agent
    ):
        """会话不存在：传入不存在的 conversation_id 应返回错误码 1003。"""
        _, token = authed_agent
        fake_id = str(uuid.uuid4())

        response = await wingman_client.post(
            f"/conversations/{fake_id}/wingman/tags",
            headers={"Authorization": f"Bearer {token}"},
        )

        data = response.json()
        assert data["code"] == 1003

    @pytest.mark.asyncio
    async def test_tags_wingman_degradation(
        self, wingman_client, authed_agent, test_conversation
    ):
        """Wingman 降级：WingmanService 返回降级标签时，API 不应 500。"""
        agent, token = authed_agent
        conv = test_conversation
        mock_wingman = wingman_client._mock_wingman

        mock_wingman.suggest_tags = AsyncMock(
            return_value={
                "suggested_tags": [],
                "category": "",
                "priority": "medium",
            }
        )

        response = await wingman_client.post(
            f"/conversations/{conv.id}/wingman/tags",
            headers={"Authorization": f"Bearer {token}"},
        )

        assert response.status_code == 200
        data = response.json()
        assert data["code"] == 0
        assert data["data"]["suggested_tags"] == []
        assert data["data"]["priority"] == "medium"
