"""MultiSourceRAG 单元测试 - 多源混合检索

测试场景：
- 指定单个信息源 → 仅从该源检索
- 指定多个信息源 → 并行检索，结果融合排序
- 不指定信息源 → 从所有可用源检索
- 来源追溯 → 每个结果包含来源信息
- AE4: 指定"合规文档库"和"法务知识库" → 仅从这两个源检索
"""

import pytest

from agentkit.memory.embedder import MockEmbedder
from agentkit.memory.knowledge_base import Document, KnowledgeBase, QueryResult, SourceInfo
from agentkit.memory.local_rag import InMemoryLocalRAGService
from agentkit.memory.multi_source_retriever import MultiSourceRetriever
from agentkit.memory.retriever import MemoryRetriever


# ── Fixtures ──────────────────────────────────────────────


@pytest.fixture
def embedder():
    return MockEmbedder(dimension=128)


@pytest.fixture
def local_rag(embedder):
    """本地合规文档库"""
    return InMemoryLocalRAGService(embedder=embedder, chunk_size=500, chunk_overlap=50)


@pytest.fixture
def legal_rag(embedder):
    """法务知识库"""
    return InMemoryLocalRAGService(embedder=embedder, chunk_size=500, chunk_overlap=50)


@pytest.fixture
def tech_rag(embedder):
    """技术文档库"""
    return InMemoryLocalRAGService(embedder=embedder, chunk_size=500, chunk_overlap=50)


@pytest.fixture
def compliance_docs():
    """合规文档"""
    return [
        Document(
            doc_id="compliance-1",
            content="数据保护合规要求：所有用户数据必须加密存储，访问需经授权审批。",
            title="数据保护合规指南",
            source_id="compliance_data_protection",
            metadata={"source": "compliance_data_protection", "format": "text"},
        ),
        Document(
            doc_id="compliance-2",
            content="跨境数据传输需遵守 GDPR 和中国网络安全法的相关规定。",
            title="跨境数据传输合规",
            source_id="compliance_cross_border",
            metadata={"source": "compliance_cross_border", "format": "text"},
        ),
    ]


@pytest.fixture
def legal_docs():
    """法务文档"""
    return [
        Document(
            doc_id="legal-1",
            content="合同审查要点：注意违约责任条款、知识产权归属和保密义务。",
            title="合同审查指南",
            source_id="legal_contract_review",
            metadata={"source": "legal_contract_review", "format": "text"},
        ),
        Document(
            doc_id="legal-2",
            content="劳动法规定：员工加班需支付加班费，标准为平时工资的1.5倍至3倍。",
            title="劳动法要点",
            source_id="legal_labor_law",
            metadata={"source": "legal_labor_law", "format": "text"},
        ),
    ]


@pytest.fixture
def tech_docs():
    """技术文档"""
    return [
        Document(
            doc_id="tech-1",
            content="API 网关配置：限流策略为每分钟 1000 次请求，超时设置 30 秒。",
            title="API 网关配置手册",
            source_id="tech_api_gateway",
            metadata={"source": "tech_api_gateway", "format": "text"},
        ),
    ]


# ── MultiSourceRetriever 核心测试 ─────────────────────────


class TestMultiSourceRetrieverBasic:
    """MultiSourceRetriever 基础功能测试"""

    def test_register_source(self, local_rag, legal_rag):
        retriever = MultiSourceRetriever()
        retriever.register_source("local:合规文档", local_rag)
        retriever.register_source("法务知识库", legal_rag)

        names = retriever.get_source_names()
        assert "local:合规文档" in names
        assert "法务知识库" in names

    def test_register_source_via_constructor(self, local_rag, legal_rag):
        retriever = MultiSourceRetriever(
            sources={"local:合规文档": local_rag, "法务知识库": legal_rag}
        )

        names = retriever.get_source_names()
        assert len(names) == 2

    def test_unregister_source(self, local_rag, legal_rag):
        retriever = MultiSourceRetriever(
            sources={"local:合规文档": local_rag, "法务知识库": legal_rag}
        )

        result = retriever.unregister_source("local:合规文档")
        assert result is True
        assert "local:合规文档" not in retriever.get_source_names()

    def test_unregister_nonexistent_source(self, local_rag):
        retriever = MultiSourceRetriever(sources={"local:合规文档": local_rag})

        result = retriever.unregister_source("不存在")
        assert result is False

    @pytest.mark.asyncio
    async def test_list_all_sources(self, local_rag, legal_rag, compliance_docs, legal_docs):
        await local_rag.ingest(compliance_docs)
        await legal_rag.ingest(legal_docs)

        retriever = MultiSourceRetriever(
            sources={"local:合规文档": local_rag, "法务知识库": legal_rag}
        )

        sources = await retriever.list_all_sources()
        assert "local:合规文档" in sources
        assert "法务知识库" in sources
        for name, info in sources.items():
            assert isinstance(info, SourceInfo)


class TestMultiSourceRetrieverSearch:
    """MultiSourceRetriever 检索功能测试"""

    @pytest.mark.asyncio
    async def test_search_single_source(
        self, local_rag, legal_rag, compliance_docs, legal_docs
    ):
        """指定单个信息源 → 仅从该源检索"""
        await local_rag.ingest(compliance_docs)
        await legal_rag.ingest(legal_docs)

        retriever = MultiSourceRetriever(
            sources={"local:合规文档": local_rag, "法务知识库": legal_rag}
        )

        # 仅从合规文档库检索
        results = await retriever.search("合规", top_k=5, sources=["local:合规文档"])

        # 所有结果应来自合规文档库
        for r in results:
            assert r.source_name == "local:合规文档"

    @pytest.mark.asyncio
    async def test_search_multiple_sources(
        self, local_rag, legal_rag, compliance_docs, legal_docs
    ):
        """指定多个信息源 → 并行检索，结果融合排序"""
        await local_rag.ingest(compliance_docs)
        await legal_rag.ingest(legal_docs)

        retriever = MultiSourceRetriever(
            sources={"local:合规文档": local_rag, "法务知识库": legal_rag}
        )

        results = await retriever.search(
            "合规 法务", top_k=10, sources=["local:合规文档", "法务知识库"]
        )

        # 结果应来自两个源
        source_names = {r.source_name for r in results}
        assert source_names.issubset({"local:合规文档", "法务知识库"})

    @pytest.mark.asyncio
    async def test_search_all_sources_when_none_specified(
        self, local_rag, legal_rag, tech_rag, compliance_docs, legal_docs, tech_docs
    ):
        """不指定信息源 → 从所有可用源检索"""
        await local_rag.ingest(compliance_docs)
        await legal_rag.ingest(legal_docs)
        await tech_rag.ingest(tech_docs)

        retriever = MultiSourceRetriever(
            sources={
                "local:合规文档": local_rag,
                "法务知识库": legal_rag,
                "技术文档库": tech_rag,
            }
        )

        results = await retriever.search("合规 法务 技术", top_k=10)

        # 结果应来自所有三个源
        source_names = {r.source_name for r in results}
        assert len(source_names) >= 1  # 至少有一个源返回结果

    @pytest.mark.asyncio
    async def test_search_no_sources_registered(self):
        """无信息源注册时返回空结果"""
        retriever = MultiSourceRetriever()

        results = await retriever.search("anything", top_k=5)
        assert len(results) == 0

    @pytest.mark.asyncio
    async def test_search_nonexistent_source(self, local_rag, compliance_docs):
        """指定不存在的源 → 跳过，返回空结果"""
        await local_rag.ingest(compliance_docs)

        retriever = MultiSourceRetriever(sources={"local:合规文档": local_rag})

        results = await retriever.search("合规", top_k=5, sources=["不存在的源"])
        assert len(results) == 0

    @pytest.mark.asyncio
    async def test_search_with_weights(
        self, local_rag, legal_rag, compliance_docs, legal_docs
    ):
        """带权重检索 → 特定源分数被调整"""
        await local_rag.ingest(compliance_docs)
        await legal_rag.ingest(legal_docs)

        retriever = MultiSourceRetriever(
            sources={"local:合规文档": local_rag, "法务知识库": legal_rag}
        )

        # 先不带权重检索
        results_no_weight = await retriever.search(
            "合规", top_k=10, sources=["local:合规文档", "法务知识库"]
        )

        # 带权重检索：提升合规文档库
        results_with_weight = await retriever.search(
            "合规",
            top_k=10,
            sources=["local:合规文档", "法务知识库"],
            weights={"local:合规文档": 2.0},
        )

        # 有权重时合规文档库的分数应更高
        compliance_scores_weighted = [
            r.score for r in results_with_weight if r.source_name == "local:合规文档"
        ]
        compliance_scores_unweighted = [
            r.score for r in results_no_weight if r.source_name == "local:合规文档"
        ]

        if compliance_scores_weighted and compliance_scores_unweighted:
            assert max(compliance_scores_weighted) >= max(compliance_scores_unweighted)


class TestMultiSourceRetrieverSourceTracing:
    """来源追溯测试"""

    @pytest.mark.asyncio
    async def test_result_contains_source_info(
        self, local_rag, legal_rag, compliance_docs, legal_docs
    ):
        """每个检索结果包含来源追溯信息"""
        await local_rag.ingest(compliance_docs)
        await legal_rag.ingest(legal_docs)

        retriever = MultiSourceRetriever(
            sources={"local:合规文档": local_rag, "法务知识库": legal_rag}
        )

        results = await retriever.search("合规", top_k=5)

        for r in results:
            # source_id 应非空
            assert r.source_id != ""
            # source_name 应为注册的源名称
            assert r.source_name in ("local:合规文档", "法务知识库")
            # title 应非空
            assert r.title != ""

    @pytest.mark.asyncio
    async def test_result_contains_document_title(
        self, local_rag, compliance_docs
    ):
        """检索结果包含文档标题"""
        await local_rag.ingest(compliance_docs)

        retriever = MultiSourceRetriever(sources={"local:合规文档": local_rag})

        results = await retriever.search("数据保护", top_k=5)

        for r in results:
            assert r.title != ""
            assert r.doc_id != ""


class TestMultiSourceRetrieverDedup:
    """去重测试"""

    @pytest.mark.asyncio
    async def test_deduplicate_identical_content(
        self, local_rag, legal_rag, embedder
    ):
        """相同内容从不同源返回时去重，保留高分"""
        # 两个源包含相同内容
        same_doc = Document(
            doc_id="same-doc",
            content="这是一段完全相同的内容用于测试去重功能。",
            title="重复文档",
            source_id="same_source",
            metadata={"source": "same_source", "format": "text"},
        )
        await local_rag.ingest([same_doc])
        await legal_rag.ingest([same_doc])

        retriever = MultiSourceRetriever(
            sources={"local:合规文档": local_rag, "法务知识库": legal_rag}
        )

        results = await retriever.search("去重", top_k=10)

        # 相同内容应去重，只保留一个
        content_counts: dict[str, int] = {}
        for r in results:
            content_counts[r.content] = content_counts.get(r.content, 0) + 1

        for content, count in content_counts.items():
            assert count == 1, f"内容 '{content[:30]}...' 出现了 {count} 次，应去重为 1 次"


# ── AE4: 合规文档库 + 法务知识库指定检索 ──────────────────


class TestAE4ComplianceAndLegalSearch:
    """AE4 场景：指定"合规文档库"和"法务知识库" → 仅从这两个源检索"""

    @pytest.mark.asyncio
    async def test_search_compliance_and_legal_only(
        self, local_rag, legal_rag, tech_rag, compliance_docs, legal_docs, tech_docs
    ):
        """指定合规和法务源 → 不从技术文档库检索"""
        await local_rag.ingest(compliance_docs)
        await legal_rag.ingest(legal_docs)
        await tech_rag.ingest(tech_docs)

        retriever = MultiSourceRetriever(
            sources={
                "合规文档库": local_rag,
                "法务知识库": legal_rag,
                "技术文档库": tech_rag,
            }
        )

        results = await retriever.search(
            "合规 法务", top_k=10, sources=["合规文档库", "法务知识库"]
        )

        # 结果不应来自技术文档库
        for r in results:
            assert r.source_name != "技术文档库"
            assert r.source_name in ("合规文档库", "法务知识库")

    @pytest.mark.asyncio
    async def test_search_compliance_and_legal_results_merged(
        self, local_rag, legal_rag, compliance_docs, legal_docs
    ):
        """合规和法务源的结果应合并排序"""
        await local_rag.ingest(compliance_docs)
        await legal_rag.ingest(legal_docs)

        retriever = MultiSourceRetriever(
            sources={"合规文档库": local_rag, "法务知识库": legal_rag}
        )

        results = await retriever.search(
            "合规 法务", top_k=10, sources=["合规文档库", "法务知识库"]
        )

        # 应有来自两个源的结果
        source_names = {r.source_name for r in results}
        assert len(source_names) >= 1

        # 结果应按 score 降序排列
        for i in range(len(results) - 1):
            assert results[i].score >= results[i + 1].score


# ── MemoryRetriever 集成测试 ──────────────────────────────


class TestMemoryRetrieverIntegration:
    """MemoryRetriever 与 MultiSourceRetriever 集成测试"""

    @pytest.mark.asyncio
    async def test_retrieve_with_sources_parameter(
        self, local_rag, legal_rag, compliance_docs, legal_docs
    ):
        """MemoryRetriever.retrieve(sources=...) 委托给 MultiSourceRetriever"""
        await local_rag.ingest(compliance_docs)
        await legal_rag.ingest(legal_docs)

        retriever = MemoryRetriever(
            knowledge_sources={"local:合规文档": local_rag, "法务知识库": legal_rag}
        )

        items = await retriever.retrieve(
            "合规", top_k=5, sources=["local:合规文档"]
        )

        # 结果应为 MemoryItem 类型
        for item in items:
            assert hasattr(item, "key")
            assert hasattr(item, "value")
            assert hasattr(item, "score")
            assert hasattr(item, "metadata")

    @pytest.mark.asyncio
    async def test_retrieve_without_sources_keeps_current_behavior(
        self, local_rag, compliance_docs
    ):
        """不指定 sources 时保持原有行为（三层记忆检索）"""
        await local_rag.ingest(compliance_docs)

        retriever = MemoryRetriever(
            knowledge_sources={"local:合规文档": local_rag}
        )

        # 不指定 sources → 走三层记忆路径
        items = await retriever.retrieve("合规", top_k=5)
        # 三层记忆为空，应返回空结果
        assert isinstance(items, list)

    @pytest.mark.asyncio
    async def test_retrieve_from_sources_with_source_tracing(
        self, local_rag, legal_rag, compliance_docs, legal_docs
    ):
        """通过 MemoryRetriever 多源检索时，结果包含来源追溯"""
        await local_rag.ingest(compliance_docs)
        await legal_rag.ingest(legal_docs)

        retriever = MemoryRetriever(
            knowledge_sources={"local:合规文档": local_rag, "法务知识库": legal_rag}
        )

        items = await retriever.retrieve(
            "合规", top_k=5, sources=["local:合规文档", "法务知识库"]
        )

        for item in items:
            assert item.metadata.get("source") == "rag"
            assert "source_name" in item.metadata
            assert "document_title" in item.metadata

    @pytest.mark.asyncio
    async def test_multi_source_retriever_property(
        self, local_rag, compliance_docs
    ):
        """通过 multi_source_retriever 属性直接访问"""
        retriever = MemoryRetriever(
            knowledge_sources={"local:合规文档": local_rag}
        )

        ms_retriever = retriever.multi_source_retriever
        assert isinstance(ms_retriever, MultiSourceRetriever)
        assert "local:合规文档" in ms_retriever.get_source_names()

    @pytest.mark.asyncio
    async def test_register_source_via_property(
        self, local_rag, legal_rag, compliance_docs, legal_docs
    ):
        """通过 multi_source_retriever 属性动态注册源"""
        await local_rag.ingest(compliance_docs)
        await legal_rag.ingest(legal_docs)

        retriever = MemoryRetriever(
            knowledge_sources={"local:合规文档": local_rag}
        )

        # 动态注册法务知识库
        retriever.multi_source_retriever.register_source("法务知识库", legal_rag)

        # 现在可以从法务知识库检索
        items = await retriever.retrieve(
            "合同", top_k=5, sources=["法务知识库"]
        )

        for item in items:
            assert item.metadata.get("source_name") == "法务知识库"

    @pytest.mark.asyncio
    async def test_retrieve_with_source_weights(
        self, local_rag, legal_rag, compliance_docs, legal_docs
    ):
        """MemoryRetriever 支持 source_weights 参数"""
        await local_rag.ingest(compliance_docs)
        await legal_rag.ingest(legal_docs)

        retriever = MemoryRetriever(
            knowledge_sources={"local:合规文档": local_rag, "法务知识库": legal_rag}
        )

        items = await retriever.retrieve(
            "合规",
            top_k=5,
            sources=["local:合规文档", "法务知识库"],
            source_weights={"local:合规文档": 1.5},
        )

        assert isinstance(items, list)


# ── 边界情况测试 ──────────────────────────────────────────


class TestEdgeCases:
    """边界情况测试"""

    @pytest.mark.asyncio
    async def test_search_with_empty_source_list(self, local_rag, compliance_docs):
        """sources=[] 空列表 → 不查询任何源，返回空"""
        await local_rag.ingest(compliance_docs)

        retriever = MultiSourceRetriever(sources={"local:合规文档": local_rag})

        results = await retriever.search("合规", top_k=5, sources=[])
        assert len(results) == 0

    @pytest.mark.asyncio
    async def test_search_top_k_limits_results(
        self, local_rag, legal_rag, compliance_docs, legal_docs
    ):
        """top_k 限制返回结果数量"""
        await local_rag.ingest(compliance_docs)
        await legal_rag.ingest(legal_docs)

        retriever = MultiSourceRetriever(
            sources={"local:合规文档": local_rag, "法务知识库": legal_rag}
        )

        results = await retriever.search("合规", top_k=1)
        assert len(results) <= 1

    @pytest.mark.asyncio
    async def test_source_query_failure_graceful(
        self, local_rag, compliance_docs
    ):
        """某个源查询失败时，其他源结果正常返回"""
        await local_rag.ingest(compliance_docs)

        # 创建一个会抛异常的 mock 源
        class FailingSource:
            async def ingest(self, documents):
                return []

            async def query(self, text, top_k=5):
                raise ConnectionError("Service unavailable")

            async def delete_by_id(self, id):
                return False

            async def list_sources(self):
                return [SourceInfo(source_id="failing", source_name="Failing", source_type="mock")]

            async def health_check(self):
                return False

        retriever = MultiSourceRetriever(
            sources={"local:合规文档": local_rag, "failing_source": FailingSource()}
        )

        # 应不抛异常，返回合规文档库的结果
        results = await retriever.search("合规", top_k=5)
        assert isinstance(results, list)

    @pytest.mark.asyncio
    async def test_search_results_sorted_by_score(
        self, local_rag, legal_rag, compliance_docs, legal_docs
    ):
        """结果按 score 降序排列"""
        await local_rag.ingest(compliance_docs)
        await legal_rag.ingest(legal_docs)

        retriever = MultiSourceRetriever(
            sources={"local:合规文档": local_rag, "法务知识库": legal_rag}
        )

        results = await retriever.search("合规 法务", top_k=10)

        for i in range(len(results) - 1):
            assert results[i].score >= results[i + 1].score
