"""RiskGuardLearner 单元测试"""

import json
from types import SimpleNamespace
from unittest.mock import AsyncMock

import pytest

from agentkit.evolution.experience_schema import TaskExperience
from agentkit.evolution.risk_guard_learner import RiskGuardLearner


def _make_experience(
    experience_id="exp1",
    task_type="code_reviewer",
    goal="review code",
    outcome="failure",
    failure_reasons=None,
    optimization_tips=None,
) -> TaskExperience:
    return TaskExperience(
        experience_id=experience_id,
        task_type=task_type,
        goal=goal,
        steps_summary="loaded skill; ran review",
        outcome=outcome,
        failure_reasons=failure_reasons or ["no code provided"],
        optimization_tips=optimization_tips or ["require code input"],
    )


def _make_llm_response(content: str):
    return SimpleNamespace(content=content)


class TestRiskGuardLearner:
    @pytest.mark.asyncio
    async def test_learn_happy_path(self):
        """3 条失败轨迹 + 合法 JSON → 返回建议"""
        store = AsyncMock()
        store.search.return_value = [
            _make_experience("e1", "code_reviewer", "review A"),
            _make_experience("e2", "code_reviewer", "review B"),
            _make_experience("e3", "code_reviewer", "review C"),
        ]
        llm = AsyncMock()
        llm.chat.return_value = _make_llm_response(
            json.dumps(
                [
                    {
                        "skill_name": "code_reviewer",
                        "precondition": "输入必须包含待审查的代码片段",
                        "reason": "多次因输入为空导致审查失败",
                        "confidence": 0.85,
                    },
                    {
                        "skill_name": "code_reviewer",
                        "precondition": "代码片段长度 >= 10 字符",
                        "reason": "过短输入无法有效审查",
                        "confidence": 0.6,
                    },
                ]
            )
        )
        learner = RiskGuardLearner(store, llm)
        suggestions = await learner.learn()
        assert len(suggestions) == 2
        assert suggestions[0].skill_name == "code_reviewer"
        assert suggestions[0].precondition == "输入必须包含待审查的代码片段"
        assert suggestions[0].confidence == 0.85
        assert set(suggestions[0].source_experience_ids) == {"e1", "e2", "e3"}

    @pytest.mark.asyncio
    async def test_learn_skill_name_filter(self):
        """skill_name 透传给 search 的 task_type"""
        store = AsyncMock()
        store.search.return_value = [_make_experience("e1", "code_reviewer")]
        llm = AsyncMock()
        llm.chat.return_value = _make_llm_response("[]")
        learner = RiskGuardLearner(store, llm)
        await learner.learn(skill_name="code_reviewer")
        store.search.assert_called_once_with(query="failure", top_k=20, task_type="code_reviewer")

    @pytest.mark.asyncio
    async def test_learn_llm_exception_returns_empty(self):
        """LLM 调用抛异常 → 返回空列表，不抛"""
        store = AsyncMock()
        store.search.return_value = [_make_experience("e1")]
        llm = AsyncMock()
        llm.chat.side_effect = RuntimeError("LLM down")
        learner = RiskGuardLearner(store, llm)
        suggestions = await learner.learn()
        assert suggestions == []

    @pytest.mark.asyncio
    async def test_learn_invalid_json_returns_empty(self):
        """LLM 返回非法 JSON → 返回空列表"""
        store = AsyncMock()
        store.search.return_value = [_make_experience("e1")]
        llm = AsyncMock()
        llm.chat.return_value = _make_llm_response("not json at all")
        learner = RiskGuardLearner(store, llm)
        suggestions = await learner.learn()
        assert suggestions == []

    @pytest.mark.asyncio
    async def test_learn_no_failures_returns_empty(self):
        """ExperienceStore 返回空 → 返回空列表，不调用 LLM"""
        store = AsyncMock()
        store.search.return_value = []
        llm = AsyncMock()
        learner = RiskGuardLearner(store, llm)
        suggestions = await learner.learn()
        assert suggestions == []
        llm.chat.assert_not_called()

    @pytest.mark.asyncio
    async def test_learn_filters_non_failure_outcomes(self):
        """只保留 outcome == 'failure' 的轨迹"""
        store = AsyncMock()
        store.search.return_value = [
            _make_experience("e1", goal="failure-goal", outcome="failure"),
            _make_experience("e2", goal="success-goal", outcome="success"),
            _make_experience("e3", goal="partial-goal", outcome="partial"),
        ]
        llm = AsyncMock()
        llm.chat.return_value = _make_llm_response("[]")
        learner = RiskGuardLearner(store, llm)
        await learner.learn()
        # 只有 e1 是 failure，prompt 中应含 failure-goal，不含 success/partial 的 goal
        call_args = llm.chat.call_args
        prompt = call_args.kwargs["messages"][1]["content"]
        assert "failure-goal" in prompt
        assert "success-goal" not in prompt
        assert "partial-goal" not in prompt

    @pytest.mark.asyncio
    async def test_confidence_clamped(self):
        """confidence 被 clamp 到 [0.0, 1.0]"""
        store = AsyncMock()
        store.search.return_value = [_make_experience("e1")]
        llm = AsyncMock()
        llm.chat.return_value = _make_llm_response(
            json.dumps(
                [
                    {"skill_name": "s", "precondition": "p1", "reason": "r", "confidence": 1.5},
                    {"skill_name": "s", "precondition": "p2", "reason": "r", "confidence": -0.3},
                    {"skill_name": "s", "precondition": "p3", "reason": "r", "confidence": 0.5},
                ]
            )
        )
        learner = RiskGuardLearner(store, llm)
        suggestions = await learner.learn()
        assert len(suggestions) == 3
        assert suggestions[0].confidence == 1.0
        assert suggestions[1].confidence == 0.0
        assert suggestions[2].confidence == 0.5

    @pytest.mark.asyncio
    async def test_learn_json_in_markdown_codeblock(self):
        """LLM 返回 markdown 代码块包裹的 JSON 也能解析"""
        store = AsyncMock()
        store.search.return_value = [_make_experience("e1")]
        llm = AsyncMock()
        llm.chat.return_value = _make_llm_response(
            '```json\n[{"skill_name":"s","precondition":"p","reason":"r","confidence":0.7}]\n```'
        )
        learner = RiskGuardLearner(store, llm)
        suggestions = await learner.learn()
        assert len(suggestions) == 1
        assert suggestions[0].precondition == "p"

    @pytest.mark.asyncio
    async def test_learn_skips_items_missing_fields(self):
        """缺少 precondition 或 skill_name 的条目被跳过"""
        store = AsyncMock()
        store.search.return_value = [_make_experience("e1")]
        llm = AsyncMock()
        llm.chat.return_value = _make_llm_response(
            json.dumps(
                [
                    {"skill_name": "s", "precondition": "", "reason": "r", "confidence": 0.5},
                    {"skill_name": "", "precondition": "p", "reason": "r", "confidence": 0.5},
                    {"skill_name": "s", "precondition": "valid", "reason": "r", "confidence": 0.5},
                ]
            )
        )
        learner = RiskGuardLearner(store, llm)
        suggestions = await learner.learn()
        assert len(suggestions) == 1
        assert suggestions[0].precondition == "valid"

    @pytest.mark.asyncio
    async def test_learn_search_exception_returns_empty(self):
        """ExperienceStore.search 抛异常 → 返回空列表"""
        store = AsyncMock()
        store.search.side_effect = RuntimeError("DB down")
        llm = AsyncMock()
        learner = RiskGuardLearner(store, llm)
        suggestions = await learner.learn()
        assert suggestions == []
