"""search_cog.py 认知搜索测试"""

from __future__ import annotations

import asyncio
from pathlib import Path

import pytest

from index1.cognitive.db_cog import CogDatabase
from index1.cognitive.search_cog import RecallResult, recall, _rrf_merge


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


def _insert_facts(cog_db: CogDatabase, n: int = 5) -> list[int]:
    ids = []
    for i in range(n):
        fid = cog_db.insert_fact({
            "collection": "test-proj",
            "assertion": f"Test fact number {i}: important detail about feature {i}",
            "confidence": 0.5 + i * 0.1,
            "session_id": "s1",
            "source": "hook",
        })
        ids.append(fid)
    return ids


class TestRecall:
    def test_basic_recall(self, cog_db: CogDatabase):
        _insert_facts(cog_db)
        result = asyncio.run(recall("feature", cog_db, collection="test-proj"))
        assert isinstance(result, RecallResult)
        assert result.total >= 1
        assert result.mode == "bm25_only"

    def test_recall_no_collection(self, cog_db: CogDatabase):
        result = asyncio.run(recall("test", cog_db))
        assert result.total == 0

    def test_recall_empty_db(self, cog_db: CogDatabase):
        result = asyncio.run(recall("anything", cog_db, collection="test-proj"))
        assert result.total == 0

    def test_recall_min_confidence_filter(self, cog_db: CogDatabase):
        cog_db.insert_fact({
            "collection": "test-proj",
            "assertion": "Low confidence fact about databases",
            "confidence": 0.1,
        })
        cog_db.insert_fact({
            "collection": "test-proj",
            "assertion": "High confidence fact about databases",
            "confidence": 0.9,
        })
        result = asyncio.run(recall(
            "databases", cog_db,
            collection="test-proj", min_confidence=0.5,
        ))
        assert all(f["confidence"] >= 0.5 for f in result.facts)

    def test_recall_top_k(self, cog_db: CogDatabase):
        _insert_facts(cog_db, n=10)
        result = asyncio.run(recall(
            "feature", cog_db,
            collection="test-proj", top_k=3,
        ))
        assert len(result.facts) <= 3

    def test_recall_touches_facts(self, cog_db: CogDatabase):
        fids = _insert_facts(cog_db, n=1)
        asyncio.run(recall("feature", cog_db, collection="test-proj"))
        fact = cog_db.get_fact(fids[0])
        assert fact["access_count"] >= 1

    def test_recall_query_ms(self, cog_db: CogDatabase):
        _insert_facts(cog_db)
        result = asyncio.run(recall("feature", cog_db, collection="test-proj"))
        assert result.query_ms >= 0

    def test_recall_facts_have_rrf_score(self, cog_db: CogDatabase):
        """#137: recall 返回的 facts 应携带 _rrf_score (归一化后的 relevance)"""
        _insert_facts(cog_db, n=3)
        result = asyncio.run(recall("feature", cog_db, collection="test-proj"))
        if result.facts:
            for f in result.facts:
                assert "_rrf_score" in f, "fact 缺少 _rrf_score 字段"
                assert 0 <= f["_rrf_score"] <= 1.0, f"_rrf_score 超出 [0,1]: {f['_rrf_score']}"
            # 第一个结果经 MaxScore 归一化后应为 1.0
            assert result.facts[0]["_rrf_score"] == 1.0

    def test_recall_scores_descending(self, cog_db: CogDatabase):
        """#137: _rrf_score 应递减排序"""
        _insert_facts(cog_db, n=5)
        result = asyncio.run(recall("feature", cog_db, collection="test-proj"))
        scores = [f["_rrf_score"] for f in result.facts]
        assert scores == sorted(scores, reverse=True)


class TestRRFMerge:
    def test_merge_disjoint(self):
        bm25 = [{"id": 1, "assertion": "a"}, {"id": 2, "assertion": "b"}]
        vec = [{"id": 3, "assertion": "c"}, {"id": 4, "assertion": "d"}]
        merged = _rrf_merge(bm25, vec)
        ids = [f["id"] for f in merged]
        assert set(ids) == {1, 2, 3, 4}

    def test_merge_overlap(self):
        bm25 = [{"id": 1, "assertion": "a"}, {"id": 2, "assertion": "b"}]
        vec = [{"id": 2, "assertion": "b"}, {"id": 3, "assertion": "c"}]
        merged = _rrf_merge(bm25, vec)
        ids = [f["id"] for f in merged]
        # id=2 appears in both, should rank higher
        assert ids[0] == 2

    def test_merge_empty(self):
        assert _rrf_merge([], []) == []
        assert len(_rrf_merge([{"id": 1}], [])) == 1

    def test_merge_propagates_rrf_score(self):
        """#137: _rrf_merge 应在返回的 dict 上附加 _rrf_score"""
        bm25 = [{"id": 1, "assertion": "a"}, {"id": 2, "assertion": "b"}]
        vec = [{"id": 2, "assertion": "b"}, {"id": 3, "assertion": "c"}]
        merged = _rrf_merge(bm25, vec)
        for f in merged:
            assert "_rrf_score" in f, f"fact {f['id']} 缺少 _rrf_score"
            assert f["_rrf_score"] > 0
        # id=2 出现在两路，分数应最高
        scores_by_id = {f["id"]: f["_rrf_score"] for f in merged}
        assert scores_by_id[2] > scores_by_id[1]
        assert scores_by_id[2] > scores_by_id[3]

    def test_merge_shallow_copy_no_mutation(self):
        """#137: 浅拷贝不污染上游 dict"""
        original_bm25 = {"id": 1, "assertion": "a"}
        original_vec = {"id": 2, "assertion": "b"}
        bm25 = [original_bm25]
        vec = [original_vec]
        merged = _rrf_merge(bm25, vec)
        # 原始 dict 不应被修改
        assert "_rrf_score" not in original_bm25
        assert "_rrf_score" not in original_vec
        # 返回的 dict 是新对象
        assert merged[0] is not original_bm25
        assert merged[1] is not original_vec

    def test_merge_setdefault_preserves_first(self):
        """#137: setdefault 保留先到版本（BM25），Vec 版本不覆盖"""
        bm25 = [{"id": 1, "assertion": "a", "highlight": "bm25_only"}]
        vec = [{"id": 1, "assertion": "a"}]  # 无 highlight
        merged = _rrf_merge(bm25, vec)
        assert len(merged) == 1
        assert merged[0]["highlight"] == "bm25_only"
