"""Agent执行逻辑单元测试 - ContentGeneratorAgent / DeAIAgent / GEOOptimizerAgent

测试策略:
- 使用 FakeLLMProvider mock LLM 调用，避免真实网络请求
- patch BaseAgent.report_progress 避免 Redis / 数据库依赖
- patch RAGService / AsyncSessionLocal 避免真实数据库访问
"""

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

import pytest

from app.agent_framework.agents.content_generator_agent import ContentGeneratorAgent
from app.agent_framework.agents.deai_agent import DeAIAgent
from app.agent_framework.agents.geo_optimizer_agent import GEOOptimizerAgent
from app.agent_framework.protocol import TaskMessage
from app.services.llm import LLMProvider, LLMResponse, LLMError


# ---------------------------------------------------------------------------
# FakeLLMProvider - 测试用假LLM
# ---------------------------------------------------------------------------
class FakeLLMProvider(LLMProvider):
    """测试用假LLM，返回预设响应"""

    def __init__(self, response_content: str = "fake response"):
        self._response = response_content

    @property
    def provider_name(self) -> str:
        return "fake"

    @property
    def model_name(self) -> str:
        return "fake-model"

    @property
    def max_context_length(self) -> int:
        return 4096

    async def chat(self, messages, **kwargs) -> LLMResponse:
        return LLMResponse(
            content=self._response,
            model="fake-model",
            usage={"prompt_tokens": 10, "completion_tokens": 20, "total_tokens": 30},
        )

    async def chat_stream(self, messages, **kwargs):
        for word in self._response.split():
            yield word + " "


# ---------------------------------------------------------------------------
# 测试辅助函数
# ---------------------------------------------------------------------------
def _make_task(task_type: str, input_data: dict) -> TaskMessage:
    return TaskMessage(
        task_id=str(uuid.uuid4()),
        agent_name="test_agent",
        task_type=task_type,
        priority=1,
        input_data=input_data,
        callback_url=None,
        created_at=datetime.now(timezone.utc),
        timeout_seconds=300,
    )


# ---------------------------------------------------------------------------
# ContentGeneratorAgent 测试
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_generate_topics_returns_parsed_json():
    """FakeLLM返回JSON数组，验证topics字段正确解析"""
    agent = ContentGeneratorAgent()
    fake_llm = FakeLLMProvider(
        response_content='[{"title": "AI营销趋势", "reason": "热门话题"}]'
    )

    with patch.object(agent, "report_progress", new_callable=AsyncMock):
        with patch(
            "app.agent_framework.agents.content_generator_agent.LLMFactory.get_default",
            return_value=fake_llm,
        ):
            task = _make_task("generate_topics", {"target_keyword": "AI营销"})
            result = await agent.execute(task)

    assert result.status == "completed"
    assert "topics" in result.output_data
    topics = result.output_data["topics"]
    assert isinstance(topics, list)
    assert topics[0]["title"] == "AI营销趋势"
    assert topics[0]["reason"] == "热门话题"


@pytest.mark.asyncio
async def test_generate_article_success():
    """验证返回content字段"""
    agent = ContentGeneratorAgent()
    fake_llm = FakeLLMProvider(response_content="这是一篇测试文章")

    with patch.object(agent, "report_progress", new_callable=AsyncMock):
        with patch(
            "app.agent_framework.agents.content_generator_agent.LLMFactory.get_default",
            return_value=fake_llm,
        ):
            task = _make_task("generate_article", {"target_keyword": "AI营销"})
            result = await agent.execute(task)

    assert result.status == "completed"
    assert result.output_data["content"] == "这是一篇测试文章"
    assert result.output_data["word_count"] == len("这是一篇测试文章")
    assert "usage" in result.output_data


@pytest.mark.asyncio
async def test_generate_with_rag_context():
    """Mock RAGService，验证知识上下文被注入"""
    agent = ContentGeneratorAgent()
    fake_llm = FakeLLMProvider(
        response_content='[{"title": "RAG测试选题", "reason": "测试"}]'
    )

    # Mock AsyncSessionLocal 上下文管理器
    mock_session = AsyncMock()
    mock_session.__aenter__ = AsyncMock(return_value=mock_session)
    mock_session.__aexit__ = AsyncMock(return_value=False)

    mock_local = MagicMock()
    mock_local.return_value.__aenter__ = AsyncMock(return_value=mock_session)
    mock_local.return_value.__aexit__ = AsyncMock(return_value=False)

    with patch.object(agent, "report_progress", new_callable=AsyncMock):
        with patch("app.database.AsyncSessionLocal", mock_local):
            with patch(
                "app.services.knowledge.rag_service.RAGService"
            ) as MockRAG:
                mock_rag = MockRAG.return_value
                mock_rag.search = AsyncMock(
                    return_value=[
                        {"document_title": "知识库文档", "content": "相关知识内容"}
                    ]
                )
                with patch(
                    "app.agent_framework.agents.content_generator_agent.LLMFactory.get_default",
                    return_value=fake_llm,
                ):
                    task = _make_task(
                        "generate_topics",
                        {"target_keyword": "AI营销", "knowledge_base_ids": ["kb-1"]},
                    )
                    result = await agent.execute(task)

    assert result.status == "completed"
    mock_rag.search.assert_awaited_once()
    # 验证 search 调用参数
    call_kwargs = mock_rag.search.call_args.kwargs
    assert call_kwargs["query"] == "AI营销"
    assert call_kwargs["knowledge_base_ids"] == ["kb-1"]


@pytest.mark.asyncio
async def test_llm_error_returns_failed():
    """Mock LLM抛出LLMError，验证返回failed状态"""
    agent = ContentGeneratorAgent()

    class ErrorLLM(FakeLLMProvider):
        async def chat(self, messages, **kwargs) -> LLMResponse:
            raise LLMError("API错误", provider="fake", status_code=500)

    error_llm = ErrorLLM()

    with patch.object(agent, "report_progress", new_callable=AsyncMock):
        with patch(
            "app.agent_framework.agents.content_generator_agent.LLMFactory.get_default",
            return_value=error_llm,
        ):
            task = _make_task("generate_topics", {"target_keyword": "AI营销"})
            result = await agent.execute(task)

    assert result.status == "failed"
    assert "LLM调用失败" in result.error_message


@pytest.mark.asyncio
async def test_extract_json_from_code_block():
    """测试```json```包裹的JSON提取"""
    agent = ContentGeneratorAgent()
    text = '```json\n[{"title": "测试"}]\n```'
    result = agent._extract_json(text)
    assert result == '[{"title": "测试"}]'


# ---------------------------------------------------------------------------
# DeAIAgent 测试
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_deai_success():
    """正常处理返回success"""
    agent = DeAIAgent()
    fake_llm = FakeLLMProvider(response_content="去AI化后的内容")

    with patch.object(agent, "report_progress", new_callable=AsyncMock):
        with patch(
            "app.agent_framework.agents.deai_agent.LLMFactory.get_default",
            return_value=fake_llm,
        ):
            task = _make_task("deai_process", {"content": "原始的AI生成内容"})
            result = await agent.execute(task)

    assert result.status == "completed"
    assert result.output_data["content"] == "去AI化后的内容"
    assert result.output_data["original_word_count"] == len("原始的AI生成内容")
    assert result.output_data["processed_word_count"] == len("去AI化后的内容")


@pytest.mark.asyncio
async def test_deai_empty_content_fails():
    """空content返回failed"""
    agent = DeAIAgent()
    fake_llm = FakeLLMProvider(response_content="something")

    with patch.object(agent, "report_progress", new_callable=AsyncMock):
        with patch(
            "app.agent_framework.agents.deai_agent.LLMFactory.get_default",
            return_value=fake_llm,
        ):
            task = _make_task("deai_process", {"content": ""})
            result = await agent.execute(task)

    assert result.status == "failed"
    # ValueError 会被外层 except 捕获，error_message 包含原始异常信息
    assert "content" in result.error_message.lower() or "input_data" in result.error_message.lower()


@pytest.mark.asyncio
async def test_deai_temperature_is_high():
    """验证调用LLM时temperature=0.9"""
    agent = DeAIAgent()
    mock_provider = AsyncMock()
    mock_provider.chat = AsyncMock(
        return_value=LLMResponse(
            content="processed",
            model="fake",
            usage={"prompt_tokens": 10, "completion_tokens": 20, "total_tokens": 30},
        )
    )

    with patch.object(agent, "report_progress", new_callable=AsyncMock):
        with patch(
            "app.agent_framework.agents.deai_agent.LLMFactory.get_default",
            return_value=mock_provider,
        ):
            task = _make_task("deai_process", {"content": "some content"})
            await agent.execute(task)

    mock_provider.chat.assert_awaited_once()
    _, kwargs = mock_provider.chat.call_args
    assert kwargs.get("temperature") == 0.9


# ---------------------------------------------------------------------------
# GEOOptimizerAgent 测试
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_geo_optimize_json_response():
    """FakeLLM返回标准JSON，验证解析"""
    agent = GEOOptimizerAgent()
    fake_llm = FakeLLMProvider(
        response_content=json.dumps(
            {
                "optimized_content": "优化后的文章",
                "seo_score": 85,
                "changes": ["优化了标题"],
            }
        )
    )

    with patch.object(agent, "report_progress", new_callable=AsyncMock):
        with patch(
            "app.agent_framework.agents.geo_optimizer_agent.LLMFactory.get_default",
            return_value=fake_llm,
        ):
            task = _make_task(
                "geo_optimize",
                {"content": "原始文章", "target_keywords": ["SEO"]},
            )
            result = await agent.execute(task)

    assert result.status == "completed"
    assert result.output_data["optimized_content"] == "优化后的文章"
    assert result.output_data["seo_score"] == 85
    assert result.output_data["changes"] == ["优化了标题"]
    assert "usage" in result.output_data


@pytest.mark.asyncio
async def test_geo_optimize_fallback():
    """FakeLLM返回纯文本，验证降级处理"""
    agent = GEOOptimizerAgent()
    fake_llm = FakeLLMProvider(response_content="这不是JSON格式")

    with patch.object(agent, "report_progress", new_callable=AsyncMock):
        with patch(
            "app.agent_framework.agents.geo_optimizer_agent.LLMFactory.get_default",
            return_value=fake_llm,
        ):
            task = _make_task(
                "geo_optimize",
                {"content": "原始文章", "target_keywords": ["SEO"]},
            )
            result = await agent.execute(task)

    assert result.status == "completed"
    assert result.output_data["optimized_content"] == "这不是JSON格式"
    assert result.output_data["seo_score"] is None
    assert result.output_data["changes"] == ["LLM输出非标准格式，已返回原始优化结果"]


@pytest.mark.asyncio
async def test_geo_optimize_keywords_in_prompt():
    """验证关键词出现在渲染后的prompt variables中"""
    agent = GEOOptimizerAgent()

    with patch.object(agent, "report_progress", new_callable=AsyncMock):
        with patch(
            "app.agent_framework.agents.geo_optimizer_agent.GEO_OPTIMIZER_TEMPLATE.render"
        ) as mock_render:
            mock_render.return_value = [
                {"role": "system", "content": "test prompt"}
            ]
            with patch(
                "app.agent_framework.agents.geo_optimizer_agent.LLMFactory.get_default"
            ) as mock_factory:
                mock_provider = AsyncMock()
                mock_provider.chat = AsyncMock(
                    return_value=LLMResponse(
                        content=json.dumps({"optimized_content": "test"}),
                        model="fake",
                        usage={},
                    )
                )
                mock_factory.return_value = mock_provider

                task = _make_task(
                    "geo_optimize",
                    {"content": "原始文章", "target_keywords": ["SEO", "GEO优化"]},
                )
                await agent.execute(task)

    mock_render.assert_called_once()
    variables = mock_render.call_args[0][0]
    assert "SEO" in variables["target_keywords"]
    assert "GEO优化" in variables["target_keywords"]
