"""Tests for VidaReflectionEngine — 周期性自我反思与记忆整合 operator。

测试覆盖：
1. 初始化：配置参数正确解析与默认值
2. reflect()：情节检索 → LLM 调用 → 语义写入完整流程
3. reflect()：情节记忆为空时跳过 LLM，返回空列表
4. reflect()：LLM 返回空内容时返回空列表
5. reflect()：LLM 调用异常时安全降级，返回空列表
6. reflect()：洞察写入语义记忆携带正确 metadata（source/timestamp/episode_count）
7. _parse_insights：bullet 行 / 非 bullet 行 / 空输出解析
8. _format_episodes：正确序号化情节记录
9. start() / stop()：后台任务生命周期管理
10. is_running / last_reflect_timestamp：属性只读正确性
"""

from __future__ import annotations

import asyncio
from unittest.mock import AsyncMock, MagicMock

from sage.vida.vida_reflection_engine import (
    LLMProtocol,
    VidaReflectionEngine,
)

# ---------------------------------------------------------------------------
# Fixtures & helpers
# ---------------------------------------------------------------------------


def _make_episodes(n: int = 3) -> list[dict]:
    return [{"id": f"ep{i}", "text": f"情节记录 {i}"} for i in range(1, n + 1)]


def _make_llm(output: str = "- 洞察一\n- 洞察二") -> AsyncMock:
    """创建实现 LLMProtocol 的异步 mock。"""
    llm = AsyncMock()
    llm.generate = AsyncMock(return_value=output)
    return llm


def _make_bridge(episodes: list[dict] | None = None) -> MagicMock:
    """创建 VidaMemoryBridge mock。"""
    bridge = MagicMock()
    bridge.recall_episodic = AsyncMock(
        return_value=episodes if episodes is not None else _make_episodes()
    )
    bridge.store_semantic = AsyncMock(return_value="semantic_id_0")
    return bridge


def _make_engine(
    episodes: list[dict] | None = None,
    llm_output: str = "- 洞察一\n- 洞察二",
    config: dict | None = None,
) -> tuple[VidaReflectionEngine, MagicMock, AsyncMock]:
    bridge = _make_bridge(episodes)
    llm = _make_llm(llm_output)
    engine = VidaReflectionEngine(memory_bridge=bridge, llm=llm, config=config or {})
    return engine, bridge, llm


# ---------------------------------------------------------------------------
# 初始化测试
# ---------------------------------------------------------------------------


class TestInit:
    """VidaReflectionEngine 初始化参数解析。"""

    def test_default_config(self):
        """未传 config 时应使用默认值。"""
        engine, _, _ = _make_engine()
        assert engine.interval_seconds == 1800
        assert engine.top_k_episodes == 10
        assert engine.reflection_query == ""
        assert "{top_k}" in engine.prompt_template

    def test_custom_config(self):
        """自定义 config 应覆盖默认值。"""
        cfg = {
            "interval_seconds": 60,
            "top_k_episodes": 5,
            "reflection_query": "天气",
            "prompt_template": "custom {top_k} {episodes}",
        }
        engine, _, _ = _make_engine(config=cfg)
        assert engine.interval_seconds == 60
        assert engine.top_k_episodes == 5
        assert engine.reflection_query == "天气"
        assert engine.prompt_template == "custom {top_k} {episodes}"

    def test_initial_state(self):
        """初始时后台任务未启动，last_reflect_timestamp 为 0。"""
        engine, _, _ = _make_engine()
        assert not engine.is_running
        assert engine.last_reflect_timestamp == 0.0


# ---------------------------------------------------------------------------
# reflect() 核心流程
# ---------------------------------------------------------------------------


class TestReflect:
    """reflect() 方法的完整召回–推理–写入流程。"""

    def test_reflect_calls_recall_episodic(self):
        """reflect() 应以 reflection_query 和 top_k_episodes 调用 recall_episodic。"""
        cfg = {"top_k_episodes": 7, "reflection_query": "记忆"}
        engine, bridge, _ = _make_engine(config=cfg)
        asyncio.run(engine.reflect())
        bridge.recall_episodic.assert_called_once_with(query="记忆", top_k=7)

    def test_reflect_calls_llm_generate(self):
        """recall_episodic 返回内容后，reflect() 应调用 llm.generate。"""
        engine, _, llm = _make_engine()
        asyncio.run(engine.reflect())
        llm.generate.assert_called_once()
        prompt_arg = llm.generate.call_args[0][0]
        assert "情节" in prompt_arg or "1." in prompt_arg  # 格式化情节文本出现在 prompt

    def test_reflect_returns_insights_list(self):
        """reflect() 应返回从 LLM 解析出的洞察字符串列表。"""
        engine, _, _ = _make_engine(llm_output="- 洞察一\n- 洞察二")
        results = asyncio.run(engine.reflect())
        assert results == ["洞察一", "洞察二"]

    def test_reflect_writes_each_insight_to_semantic(self):
        """每条洞察都应写入语义记忆（store_semantic 被调用相应次数）。"""
        engine, bridge, _ = _make_engine(llm_output="- A\n- B\n- C")
        asyncio.run(engine.reflect())
        assert bridge.store_semantic.call_count == 3

    def test_reflect_metadata_has_required_keys(self):
        """写入语义记忆时，metadata 应包含 source/timestamp/episode_count。"""
        engine, bridge, _ = _make_engine(llm_output="- 洞察X")
        asyncio.run(engine.reflect())
        _, kwargs = bridge.store_semantic.call_args
        meta = kwargs.get("metadata") or bridge.store_semantic.call_args[0][1]
        assert meta["source"] == "reflection"
        assert isinstance(meta["timestamp"], float)
        assert meta["episode_count"] == 3  # default _make_episodes(3)

    def test_reflect_updates_last_reflect_timestamp(self):
        """成功反思后 last_reflect_timestamp 应更新为非零值。"""
        engine, _, _ = _make_engine()
        asyncio.run(engine.reflect())
        assert engine.last_reflect_timestamp > 0.0


# ---------------------------------------------------------------------------
# reflect() 边界条件
# ---------------------------------------------------------------------------


class TestReflectEdgeCases:
    """reflect() 在空情节/空输出/LLM 异常等边界场景的行为。"""

    def test_reflect_empty_episodes_skips_llm(self):
        """情节记忆为空时，不应调用 LLM，返回空列表。"""
        engine, bridge, llm = _make_engine(episodes=[])
        results = asyncio.run(engine.reflect())
        llm.generate.assert_not_called()
        bridge.store_semantic.assert_not_called()
        assert results == []

    def test_reflect_llm_returns_empty_string(self):
        """LLM 返回空字符串时，返回空列表，不写语义记忆。"""
        engine, bridge, _ = _make_engine(llm_output="")
        results = asyncio.run(engine.reflect())
        bridge.store_semantic.assert_not_called()
        assert results == []

    def test_reflect_llm_returns_whitespace_only(self):
        """LLM 返回纯空白时，返回空列表。"""
        engine, bridge, _ = _make_engine(llm_output="   \n\t  ")
        results = asyncio.run(engine.reflect())
        assert results == []

    def test_reflect_llm_exception_returns_empty(self):
        """LLM 抛出异常时，reflect() 应安全降级返回空列表，不抛出。"""
        bridge = _make_bridge()
        llm = AsyncMock()
        llm.generate = AsyncMock(side_effect=RuntimeError("llm down"))
        engine = VidaReflectionEngine(bridge, llm)
        results = asyncio.run(engine.reflect())
        assert results == []
        bridge.store_semantic.assert_not_called()

    def test_reflect_llm_exception_does_not_update_timestamp(self):
        """LLM 异常后，last_reflect_timestamp 不应更新。"""
        bridge = _make_bridge()
        llm = AsyncMock()
        llm.generate = AsyncMock(side_effect=RuntimeError("down"))
        engine = VidaReflectionEngine(bridge, llm)
        asyncio.run(engine.reflect())
        assert engine.last_reflect_timestamp == 0.0


# ---------------------------------------------------------------------------
# _parse_insights 单元测试
# ---------------------------------------------------------------------------


class TestParseInsights:
    """_parse_insights 静态方法的文本解析行为。"""

    def test_bullet_lines_extracted(self):
        raw = "- 洞察一\n- 洞察二\n- 洞察三"
        result = VidaReflectionEngine._parse_insights(raw)
        assert result == ["洞察一", "洞察二", "洞察三"]

    def test_bullet_with_extra_whitespace(self):
        raw = "-  头尾空格  \n-  另一条  "
        result = VidaReflectionEngine._parse_insights(raw)
        assert result == ["头尾空格", "另一条"]

    def test_no_bullet_fallback_to_lines(self):
        """无 bullet 行时，逐行回退，每非空行作为一条洞察。"""
        raw = "洞察A\n洞察B"
        result = VidaReflectionEngine._parse_insights(raw)
        assert result == ["洞察A", "洞察B"]

    def test_empty_string_returns_empty(self):
        assert VidaReflectionEngine._parse_insights("") == []

    def test_whitespace_only_returns_empty(self):
        assert VidaReflectionEngine._parse_insights("   \n  ") == []

    def test_mixed_bullet_and_plain_extracts_bullets(self):
        """同时存在 bullet 行和普通行时，只提取 bullet 行。"""
        raw = "前置说明行\n- 洞察X\n- 洞察Y"
        result = VidaReflectionEngine._parse_insights(raw)
        assert result == ["洞察X", "洞察Y"]

    def test_bullet_prefix_symbol(self):
        """支持 「• 」前缀的 bullet 行。"""
        raw = "• 洞察一\n• 洞察二"
        result = VidaReflectionEngine._parse_insights(raw)
        assert result == ["洞察一", "洞察二"]


# ---------------------------------------------------------------------------
# _format_episodes 单元测试
# ---------------------------------------------------------------------------


class TestFormatEpisodes:
    """_format_episodes 静态方法的格式化行为。"""

    def test_numbered_lines(self):
        episodes = [{"text": "情节A"}, {"text": "情节B"}]
        result = VidaReflectionEngine._format_episodes(episodes)
        assert "1. 情节A" in result
        assert "2. 情节B" in result

    def test_empty_list_returns_empty_string(self):
        result = VidaReflectionEngine._format_episodes([])
        assert result == ""

    def test_fallback_to_content_key(self):
        """若 text 键不存在，应回退至 content 键。"""
        episodes = [{"content": "内容X"}]
        result = VidaReflectionEngine._format_episodes(episodes)
        assert "内容X" in result

    def test_fallback_to_str(self):
        """既无 text 也无 content 时，str(ep) 作为内容。"""
        episodes = [{"other_key": "数据"}]
        result = VidaReflectionEngine._format_episodes(episodes)
        assert "1." in result  # 至少序号存在


# ---------------------------------------------------------------------------
# 后台任务生命周期
# ---------------------------------------------------------------------------


class TestBackgroundTask:
    """start() / stop() 后台定时任务生命周期。"""

    def test_start_sets_is_running(self):
        """start() 后 is_running 应为 True。"""
        engine, _, _ = _make_engine(config={"interval_seconds": 9999})

        async def _run():
            await engine.start()
            assert engine.is_running
            await engine.stop()

        asyncio.run(_run())

    def test_stop_clears_is_running(self):
        """stop() 后 is_running 应为 False。"""
        engine, _, _ = _make_engine(config={"interval_seconds": 9999})

        async def _run():
            await engine.start()
            await engine.stop()
            assert not engine.is_running

        asyncio.run(_run())

    def test_start_twice_returns_same_task(self):
        """重复调用 start() 不应创建多个任务。"""
        engine, _, _ = _make_engine(config={"interval_seconds": 9999})

        async def _run():
            t1 = await engine.start()
            t2 = await engine.start()
            assert t1 is t2
            await engine.stop()

        asyncio.run(_run())

    def test_background_loop_calls_reflect(self):
        """后台循环应在 interval 到期后调用 reflect()。"""
        engine, bridge, _ = _make_engine(config={"interval_seconds": 0})

        async def _run():
            await engine.start()
            # 给事件循环机会执行至少一轮 reflect
            await asyncio.sleep(0.05)
            await engine.stop()

        asyncio.run(_run())
        # 至少触发过一次 recall_episodic（即 reflect 被调用）
        assert bridge.recall_episodic.call_count >= 1


# ---------------------------------------------------------------------------
# LLMProtocol 协议一致性
# ---------------------------------------------------------------------------


class TestLLMProtocol:
    """LLMProtocol runtime_checkable 协议验证。"""

    def test_mock_llm_satisfies_protocol(self):
        llm = _make_llm()
        assert isinstance(llm, LLMProtocol)

    def test_object_without_generate_fails(self):
        class BadLLM:
            pass

        assert not isinstance(BadLLM(), LLMProtocol)
