"""MCP tools 测试 — 5 tools: recall, learn, status, reindex, config

不启动 MCP 服务器，直接测试工具函数逻辑。
"""

from __future__ import annotations

import asyncio
from pathlib import Path
from unittest.mock import MagicMock

import pytest

from index1.cognitive.db_cog import CogDatabase
from index1.config import Config


@pytest.fixture()
def cog_db(tmp_path: Path) -> CogDatabase:
    db = CogDatabase(tmp_path / "mcp_cog_test.db", embedding_dim=4)
    db.connect()
    yield db
    db.close()


@pytest.fixture()
def app_ctx(cog_db: CogDatabase) -> MagicMock:
    """模拟 AppContext"""
    ctx = MagicMock()
    ctx.db_cog = cog_db
    ctx.embedder = MagicMock()
    ctx.embedder.available = False
    ctx.config = Config()
    return ctx


@pytest.fixture()
def mock_mcp_ctx(app_ctx) -> MagicMock:
    """模拟 MCP Context"""
    ctx = MagicMock()
    ctx.request_context.lifespan_context = app_ctx
    return ctx


class TestLearn:
    def test_basic_learn(self, mock_mcp_ctx, cog_db):
        from index1.mcp_server import learn

        result = asyncio.run(learn(
            insight="SQLite FTS5 需要手动同步索引",
            ctx=mock_mcp_ctx,
            collection="test-proj",
        ))
        assert result["status"] == "created"
        assert result["fact_id"] >= 1
        assert "auto_inferred" in result

        fact = cog_db.get_fact(result["fact_id"])
        assert fact["assertion"] == "SQLite FTS5 需要手动同步索引"
        assert fact["source"] == "claude"

    def test_learn_auto_category_decision(self, mock_mcp_ctx, cog_db):
        from index1.mcp_server import learn

        result = asyncio.run(learn(
            insight="chose ONNX instead of Ollama for default embedding",
            ctx=mock_mcp_ctx,
            collection="test-proj",
        ))
        assert result["status"] == "created"
        assert result["auto_inferred"]["category"] == "decision"

    def test_learn_auto_category_root_cause(self, mock_mcp_ctx, cog_db):
        from index1.mcp_server import learn

        result = asyncio.run(learn(
            insight="the bug was caused by a race condition in db writes",
            ctx=mock_mcp_ctx,
            collection="test-proj",
        ))
        assert result["auto_inferred"]["category"] == "root_cause"

    def test_learn_auto_scope_files(self, mock_mcp_ctx, cog_db):
        from index1.mcp_server import learn

        result = asyncio.run(learn(
            insight="issue in src/index1/db.py with `insert_fact()` locking",
            ctx=mock_mcp_ctx,
            collection="test-proj",
        ))
        assert "src/index1/db.py" in result["auto_inferred"]["scope_files"]
        assert "insert_fact" in result["auto_inferred"]["scope_entities"]

    def test_learn_with_context(self, mock_mcp_ctx, cog_db):
        from index1.mcp_server import learn

        result = asyncio.run(learn(
            insight="WAL mode is essential for concurrency",
            ctx=mock_mcp_ctx,
            context="discovered during stress testing with 5 concurrent writers",
            collection="test-proj",
        ))
        assert result["status"] == "created"
        # confidence 应该 > 0.8（有 context boost）
        assert result["auto_inferred"]["confidence"] > 0.8

    def test_learn_dedup_exact(self, mock_mcp_ctx, cog_db):
        from index1.mcp_server import learn

        # 第一次写入
        r1 = asyncio.run(learn(
            insight="BEGIN IMMEDIATE solves busy timeout issue",
            ctx=mock_mcp_ctx,
            collection="test-proj",
        ))
        assert r1["status"] == "created"

        # 第二次相同 insight → duplicate
        r2 = asyncio.run(learn(
            insight="BEGIN IMMEDIATE solves busy timeout issue",
            ctx=mock_mcp_ctx,
            collection="test-proj",
        ))
        assert r2["status"] == "duplicate"
        assert r2["method"] == "exact"

    def test_learn_dedup_jaccard(self, mock_mcp_ctx, cog_db):
        from index1.mcp_server import learn

        r1 = asyncio.run(learn(
            insight="Fixed critical authentication bug causing login failure",
            ctx=mock_mcp_ctx,
            collection="test-proj",
        ))
        assert r1["status"] == "created"

        # 非常相似的 insight → duplicate via jaccard
        # 停用词过滤后仍然有高重叠
        r2 = asyncio.run(learn(
            insight="Fixed critical authentication bug causing login crash",
            ctx=mock_mcp_ctx,
            collection="test-proj",
        ))
        assert r2["status"] == "duplicate"
        assert r2["method"] == "jaccard"

    def test_learn_disabled(self, mock_mcp_ctx):
        from index1.mcp_server import learn

        mock_mcp_ctx.request_context.lifespan_context.db_cog = None
        result = asyncio.run(learn(
            insight="test insight",
            ctx=mock_mcp_ctx,
        ))
        assert "error" in result

    def test_learn_return_structure(self, mock_mcp_ctx, cog_db):
        from index1.mcp_server import learn

        result = asyncio.run(learn(
            insight="new observation about caching strategy",
            ctx=mock_mcp_ctx,
            collection="test-proj",
        ))
        assert "status" in result
        assert "fact_id" in result
        assert "auto_inferred" in result
        inferred = result["auto_inferred"]
        assert "category" in inferred
        assert "confidence" in inferred
        assert "scope_files" in inferred
        assert "scope_entities" in inferred
        assert "scope_modules" in inferred
        assert "similar_facts" in result


class TestRecallTool:
    """recall MCP 工具测试 — 统一搜索接口"""

    def test_basic_recall(self, mock_mcp_ctx, cog_db, app_ctx):
        """基本搜索 — 有 corpus 和 cognition 结果"""
        from unittest.mock import AsyncMock, patch
        from index1.mcp_server import recall
        from index1.search import SearchResponse, SearchResult

        # 准备 cognition 数据
        cog_db.insert_fact({
            "collection": "test-proj",
            "assertion": "Database uses connection pooling for performance",
            "confidence": 0.9,
        })

        # Mock corpus search
        corpus_resp = SearchResponse(
            results=[SearchResult(
                chunk_id=1, score=0.95,
                source="src/db.py", section="connect",
                summary="Connection setup", relevance="high",
                content="def connect(): ...",
            )],
            total=1, mode="hybrid",
        )

        # 需要 mock corpus db 的 symbols 查询
        app_ctx.db = MagicMock()
        app_ctx.db.conn.execute.return_value.fetchall.return_value = [("connect",)]

        with patch("index1.unified_search.corpus_search", new_callable=AsyncMock) as mock_cs:
            mock_cs.return_value = corpus_resp
            result = asyncio.run(recall(
                query="database connection",
                ctx=mock_mcp_ctx,
                collection="test-proj",
            ))

        # 分组结构验证
        assert "code" in result
        assert "facts" in result
        assert "tactics" in result
        assert "model" in result
        assert "mode" in result
        assert "hint" in result
        assert "query_ms" in result

        # code 结果
        assert len(result["code"]) == 1
        assert result["code"][0]["source"] == "src/db.py"
        assert "symbols" in result["code"][0]

    def test_recall_cognition_disabled(self, mock_mcp_ctx, app_ctx):
        """db_cog=None 时只返回 corpus 结果"""
        from unittest.mock import AsyncMock, patch
        from index1.mcp_server import recall
        from index1.search import SearchResponse

        app_ctx.db_cog = None
        app_ctx.db = MagicMock()
        app_ctx.db.conn.execute.return_value.fetchall.return_value = []

        with patch("index1.unified_search.corpus_search", new_callable=AsyncMock) as mock_cs:
            mock_cs.return_value = SearchResponse(results=[], total=0)
            result = asyncio.run(recall(
                query="test", ctx=mock_mcp_ctx,
            ))

        assert result["facts"] == []
        assert result["tactics"] == []
        assert result["model"] is None

    def test_recall_return_structure(self, mock_mcp_ctx, cog_db, app_ctx):
        """返回结构完整性验证"""
        from unittest.mock import AsyncMock, patch
        from index1.mcp_server import recall
        from index1.search import SearchResponse

        app_ctx.db = MagicMock()
        app_ctx.db.conn.execute.return_value.fetchall.return_value = []

        with patch("index1.unified_search.corpus_search", new_callable=AsyncMock) as mock_cs:
            mock_cs.return_value = SearchResponse(results=[], total=0)
            result = asyncio.run(recall(
                query="test", ctx=mock_mcp_ctx, collection="test-proj",
            ))

        # 所有顶层键存在
        for key in ("code", "facts", "tactics", "model", "mode", "hint", "query_ms"):
            assert key in result, f"Missing key: {key}"

    def test_recall_embed_hint_when_unavailable(self, mock_mcp_ctx, app_ctx):
        """embedding 不可用时返回提示"""
        from unittest.mock import AsyncMock, patch
        from index1.mcp_server import recall
        from index1.search import SearchResponse

        app_ctx.embedder.available = False
        app_ctx.embedder.unavailable_reason = "Ollama not running"
        app_ctx.db_cog = None
        app_ctx.db = MagicMock()
        app_ctx.db.conn.execute.return_value.fetchall.return_value = []

        with patch("index1.unified_search.corpus_search", new_callable=AsyncMock) as mock_cs:
            mock_cs.return_value = SearchResponse(results=[], total=0)
            result = asyncio.run(recall(
                query="test", ctx=mock_mcp_ctx,
            ))

        assert result["embed_status"] == "unavailable"
        assert "embed_hint" in result
