# -*- coding: utf-8 -*-
"""memory_save / memory_search 工具单元测试"""
import os
import sys
from pathlib import Path
from unittest.mock import MagicMock

import pytest

sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))

from abyss.memory.store import MemoryStore, MemoryEntry
from abyss.memory.bm25 import BM25Engine
from abyss.tools.memory_save import MemorySaveTool
from abyss.tools.memory_search import MemorySearchTool


@pytest.fixture
def store(tmp_path):
    return MemoryStore(tmp_path / "memory.jsonl")


class TestMemorySaveTool:
    def test_save_success(self, store):
        tool = MemorySaveTool(store=store)
        result = tool.execute(
            title="Python 调试技巧",
            content="使用 pdb 进行断点调试",
            category="fact",
            tags=["python", "debug"],
        )
        assert result["success"] is True
        assert "id" in result
        assert result["title"] == "Python 调试技巧"
        assert result["category"] == "fact"
        # 已持久化
        assert store.count == 1

    def test_save_minimal_args(self, store):
        tool = MemorySaveTool(store=store)
        result = tool.execute(title="测试", content="内容")
        assert result["success"] is True
        assert result["category"] == "session"  # 默认值

    def test_save_empty_title(self, store):
        tool = MemorySaveTool(store=store)
        result = tool.execute(title="", content="内容")
        assert result["success"] is False
        assert "title" in result["error"]

    def test_save_empty_content(self, store):
        tool = MemorySaveTool(store=store)
        result = tool.execute(title="标题", content="")
        assert result["success"] is False
        assert "content" in result["error"]

    def test_save_invalid_category_defaults_to_session(self, store):
        tool = MemorySaveTool(store=store)
        result = tool.execute(
            title="t", content="c", category="invalid_category"
        )
        assert result["success"] is True
        assert result["category"] == "session"

    def test_save_with_no_store(self):
        """store 为 None 时返回错误而非抛异常"""
        tool = MemorySaveTool(store=None)
        result = tool.execute(title="t", content="c")
        assert result["success"] is False
        assert "未加载" in result["error"]

    def test_save_tags_string_input(self, store):
        """tags 字符串应被规整"""
        tool = MemorySaveTool(store=store)
        # LLM 可能传 JSON 字符串
        result = tool.execute(
            title="t", content="c", tags='["tag1", "tag2"]'
        )
        assert result["success"] is True

    def test_tool_interface(self, store):
        """Tool 接口：name / description / parameters / to_openai_schema"""
        tool = MemorySaveTool(store=store)
        assert tool.name == "memory_save"
        assert isinstance(tool.description, str)
        assert len(tool.description) > 0
        params = tool.parameters
        assert params["type"] == "object"
        assert "title" in params["properties"]
        assert "content" in params["properties"]
        assert "title" in params["required"]
        assert "content" in params["required"]

        schema = tool.to_openai_schema()
        assert schema["type"] == "function"
        assert schema["function"]["name"] == "memory_save"


class TestMemorySearchTool:
    def test_search_basic(self, store):
        """基础检索"""
        store.add(MemoryEntry(id="m1", title="Python 入门", content="Python 编程基础"))
        store.add(MemoryEntry(id="m2", title="Java 入门", content="Java 编程基础"))
        tool = MemorySearchTool(store=store)
        result = tool.execute(query="Python", top_k=2)
        assert result["success"] is True
        assert len(result["results"]) >= 1
        # 应返回 Python 相关
        assert any("Python" in r["title"] for r in result["results"])

    def test_search_empty_store(self, store):
        tool = MemorySearchTool(store=store)
        result = tool.execute(query="foo")
        assert result["success"] is True
        assert result["results"] == []

    def test_search_no_match(self, store):
        store.add(MemoryEntry(id="m1", title="foo", content="bar"))
        tool = MemorySearchTool(store=store)
        result = tool.execute(query="nonexistent")
        assert result["success"] is True
        assert result["results"] == []

    def test_search_empty_query(self, store):
        tool = MemorySearchTool(store=store)
        result = tool.execute(query="")
        assert result["success"] is False
        assert "query" in result["error"]

    def test_search_top_k_limit(self, store):
        """top_k 限制"""
        for i in range(5):
            store.add(MemoryEntry(
                id=f"m{i}",
                title=f"Python 入门 {i}",
                content=f"Python 编程 {i}",
            ))
        tool = MemorySearchTool(store=store)
        result = tool.execute(query="Python", top_k=2)
        assert result["success"] is True
        assert len(result["results"]) <= 2

    def test_search_top_k_max_cap(self, store):
        """top_k 超过 10 应被截断到 10"""
        for i in range(15):
            store.add(MemoryEntry(id=f"m{i}", title=f"t{i}", content=f"python code {i}"))
        tool = MemorySearchTool(store=store)
        result = tool.execute(query="python", top_k=100)
        assert result["success"] is True
        assert len(result["results"]) <= 10

    def test_search_with_reranker(self, store):
        """启用 reranker 时调用 reranker.rerank"""
        store.add(MemoryEntry(id="m1", title="Python 入门", content="Python 基础"))
        store.add(MemoryEntry(id="m2", title="Python 进阶", content="Python 高级特性"))
        reranker = MagicMock()
        reranker.enabled = True
        # reranker 返回 [1, 0]（即倒序）
        reranker.rerank.return_value = [1, 0]
        tool = MemorySearchTool(store=store, reranker=reranker)
        result = tool.execute(query="Python", top_k=2)
        assert result["success"] is True
        assert len(result["results"]) == 2
        # 应按 reranker 返回的顺序：[1, 0] → 第二条在前
        assert result["results"][0]["id"] == "m2"
        assert result["results"][1]["id"] == "m1"
        reranker.rerank.assert_called_once()

    def test_search_reranker_disabled(self, store):
        """reranker.enabled=False 时跳过 rerank"""
        store.add(MemoryEntry(id="m1", title="Python 入门", content="Python 基础"))
        store.add(MemoryEntry(id="m2", title="Python 进阶", content="Python 高级"))
        reranker = MagicMock()
        reranker.enabled = False
        tool = MemorySearchTool(store=store, reranker=reranker)
        result = tool.execute(query="Python", top_k=2)
        assert result["success"] is True
        # reranker 不应被调用
        reranker.rerank.assert_not_called()

    def test_search_reranker_exception_fallback(self, store):
        """reranker 抛异常时应降级到 BM25 顺序"""
        store.add(MemoryEntry(id="m1", title="Python 入门", content="Python 基础"))
        store.add(MemoryEntry(id="m2", title="Python 进阶", content="Python 高级"))
        reranker = MagicMock()
        reranker.enabled = True
        reranker.rerank.side_effect = RuntimeError("LLM 失败")
        tool = MemorySearchTool(store=store, reranker=reranker)
        result = tool.execute(query="Python", top_k=2)
        assert result["success"] is True
        assert len(result["results"]) >= 1

    def test_search_with_no_store(self):
        tool = MemorySearchTool(store=None)
        result = tool.execute(query="foo")
        assert result["success"] is False
        assert "未加载" in result["error"]

    def test_search_string_top_k(self, store):
        """top_k 字符串应被规整为 int"""
        store.add(MemoryEntry(id="m1", title="python", content="code"))
        tool = MemorySearchTool(store=store)
        result = tool.execute(query="python", top_k="3")
        assert result["success"] is True

    def test_search_cjk_query(self, store):
        """中文查询"""
        store.add(MemoryEntry(id="m1", title="调试技巧", content="使用 pdb 调试 Python"))
        store.add(MemoryEntry(id="m2", title="测试方法", content="使用 pytest 测试"))
        tool = MemorySearchTool(store=store)
        result = tool.execute(query="调试", top_k=2)
        assert result["success"] is True
        # 应优先匹配调试相关
        assert any("调试" in r["title"] for r in result["results"])

    def test_tool_interface(self, store):
        tool = MemorySearchTool(store=store)
        assert tool.name == "memory_search"
        assert isinstance(tool.description, str)
        params = tool.parameters
        assert params["type"] == "object"
        assert "query" in params["properties"]
        assert "query" in params["required"]
        schema = tool.to_openai_schema()
        assert schema["function"]["name"] == "memory_search"

    def test_custom_bm25_factory(self, store):
        """支持注入自定义 BM25Engine 工厂"""
        mock_engine = MagicMock()
        mock_engine.search.return_value = []
        mock_engine.index.return_value = None
        tool = MemorySearchTool(
            store=store,
            bm25_factory=lambda: mock_engine,
        )
        store.add(MemoryEntry(id="m1", title="t", content="c"))
        result = tool.execute(query="foo")
        assert result["success"] is True
        mock_engine.index.assert_called_once()
        mock_engine.search.assert_called_once()
