"""
Integration tests for Metadata Governance runtime pipeline.
[AC-IDSMETA-18~20] Test routing -> filtering -> retrieval -> fallback chain.

Test Matrix:
- AC-IDSMETA-18: Intent routing with target KB selection
- AC-IDSMETA-19: Metadata filter injection in RAG retrieval
- AC-IDSMETA-20: Fallback strategy with structured reason codes
"""

import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from typing import Any
from dataclasses import dataclass, field


@dataclass
class MockIntentRule:
    """Mock IntentRule for testing."""
    id: str
    name: str
    response_type: str
    target_kb_ids: list[str] | None = None
    keywords: list[str] | None = None
    patterns: list[str] | None = None
    priority: int = 0
    is_enabled: bool = True
    fixed_reply: str | None = None
    flow_id: str | None = None
    transfer_message: str | None = None


@dataclass
class MockIntentMatchResult:
    """Mock IntentMatchResult for testing."""
    rule: MockIntentRule
    match_type: str
    matched: str

    def to_dict(self) -> dict[str, Any]:
        return {
            "rule_id": str(self.rule.id),
            "rule_name": self.rule.name,
            "match_type": self.match_type,
            "matched": self.matched,
            "response_type": self.rule.response_type,
            "target_kb_ids": self.rule.target_kb_ids or [],
        }


@dataclass
class MockRetrievalHit:
    """Mock RetrievalHit for testing."""
    text: str
    score: float
    source: str
    metadata: dict[str, Any] = field(default_factory=dict)


@dataclass
class MockRetrievalResult:
    """Mock RetrievalResult for testing."""
    hits: list[MockRetrievalHit]
    diagnostics: dict[str, Any] = field(default_factory=dict)

    @property
    def hit_count(self) -> int:
        return len(self.hits)

    @property
    def max_score(self) -> float:
        return max((h.score for h in self.hits), default=0.0)

    @property
    def is_empty(self) -> bool:
        return len(self.hits) == 0


class MockIntentRouter:
    """Mock IntentRouter for testing."""

    def match(self, message: str, rules: list[MockIntentRule]) -> MockIntentMatchResult | None:
        message_lower = message.lower()
        sorted_rules = sorted(rules, key=lambda r: r.priority, reverse=True)
        for rule in sorted_rules:
            if not rule.is_enabled:
                continue
            if rule.keywords:
                for keyword in rule.keywords:
                    if keyword.lower() in message_lower:
                        return MockIntentMatchResult(
                            rule=rule,
                            match_type="keyword",
                            matched=keyword,
                        )
            if rule.patterns:
                import re
                for pattern in rule.patterns:
                    if re.search(pattern, message, re.IGNORECASE):
                        return MockIntentMatchResult(
                            rule=rule,
                            match_type="regex",
                            matched=pattern,
                        )
        return None


class MockRetriever:
    """Mock Retriever with metadata filtering support."""

    def __init__(self, hits: list[MockRetrievalHit] | None = None):
        self._hits = hits or []
        self._last_filter: dict[str, Any] | None = None
        self._last_target_kb_ids: list[str] | None = None

    async def retrieve(
        self,
        tenant_id: str,
        query: str,
        target_kb_ids: list[str] | None = None,
        metadata_filter: dict[str, Any] | None = None,
    ) -> MockRetrievalResult:
        self._last_filter = metadata_filter
        self._last_target_kb_ids = target_kb_ids

        filtered_hits = []
        for hit in self._hits:
            if metadata_filter:
                match = True
                for key, value in metadata_filter.items():
                    if hit.metadata.get(key) != value:
                        match = False
                        break
                if not match:
                    continue
            if target_kb_ids:
                if hit.metadata.get("kb_id") not in target_kb_ids:
                    continue
            filtered_hits.append(hit)

        return MockRetrievalResult(
            hits=filtered_hits,
            diagnostics={
                "filter_applied": metadata_filter is not None,
                "target_kb_ids": target_kb_ids,
            },
        )


class FallbackStrategy:
    """
    [AC-IDSMETA-20] Fallback strategy with structured reason codes.
    """

    REASON_CODES = {
        "NO_INTENT_MATCH": "intent_not_matched",
        "NO_RETRIEVAL_HITS": "retrieval_empty",
        "LOW_CONFIDENCE": "confidence_below_threshold",
        "KB_UNAVAILABLE": "knowledge_base_unavailable",
        "METADATA_FILTER_TOO_STRICT": "filter_excluded_all",
    }

    def execute(
        self,
        reason: str,
        fallback_kb_id: str | None = None,
        fallback_message: str | None = None,
    ) -> dict[str, Any]:
        reason_code = self.REASON_CODES.get(reason, "unknown")

        result = {
            "fallback_triggered": True,
            "reason_code": reason_code,
            "fallback_type": None,
            "fallback_content": None,
        }

        if fallback_kb_id:
            result["fallback_type"] = "kb"
            result["fallback_kb_id"] = fallback_kb_id
        elif fallback_message:
            result["fallback_type"] = "fixed"
            result["fallback_content"] = fallback_message
        else:
            result["fallback_type"] = "default"
            result["fallback_content"] = "抱歉，我暂时无法回答您的问题，请稍后重试或联系人工客服。"

        return result


class TestIntentRouting:
    """
    [AC-IDSMETA-18] Test intent routing with target KB selection.
    """

    def setup_method(self):
        self.router = MockIntentRouter()

    def test_keyword_match_routes_to_rag(self):
        """Intent with response_type=rag should route to RAG with target KBs."""
        rules = [
            MockIntentRule(
                id="rule-1",
                name="退货咨询",
                response_type="rag",
                target_kb_ids=["kb-return", "kb-policy"],
                keywords=["退货", "退款"],
                priority=10,
            )
        ]

        result = self.router.match("我想退货怎么办", rules)

        assert result is not None
        assert result.rule.response_type == "rag"
        assert result.rule.target_kb_ids == ["kb-return", "kb-policy"]
        assert result.match_type == "keyword"

    def test_regex_match_routes_to_flow(self):
        """Intent with response_type=flow should start script flow."""
        rules = [
            MockIntentRule(
                id="rule-2",
                name="订单查询",
                response_type="flow",
                flow_id="flow-order-query",
                patterns=[r"订单.*查询", r"查询.*订单"],
                priority=5,
            )
        ]

        result = self.router.match("帮我查询订单状态", rules)

        assert result is not None
        assert result.rule.response_type == "flow"
        assert result.rule.flow_id == "flow-order-query"
        assert result.match_type == "regex"

    def test_fixed_reply_intent(self):
        """Intent with response_type=fixed should return fixed reply."""
        rules = [
            MockIntentRule(
                id="rule-3",
                name="问候",
                response_type="fixed",
                fixed_reply="您好，请问有什么可以帮您？",
                keywords=["你好", "您好"],
                priority=1,
            )
        ]

        result = self.router.match("你好", rules)

        assert result is not None
        assert result.rule.response_type == "fixed"
        assert result.rule.fixed_reply == "您好，请问有什么可以帮您？"

    def test_transfer_intent(self):
        """Intent with response_type=transfer should trigger transfer."""
        rules = [
            MockIntentRule(
                id="rule-4",
                name="人工服务",
                response_type="transfer",
                transfer_message="正在为您转接人工客服...",
                keywords=["人工", "转人工"],
                priority=100,
            )
        ]

        result = self.router.match("我要转人工", rules)

        assert result is not None
        assert result.rule.response_type == "transfer"
        assert result.rule.transfer_message == "正在为您转接人工客服..."

    def test_priority_ordering(self):
        """Higher priority rules should be matched first."""
        rules = [
            MockIntentRule(
                id="rule-low",
                name="通用问候",
                response_type="fixed",
                fixed_reply="通用问候回复",
                keywords=["你好"],
                priority=1,
            ),
            MockIntentRule(
                id="rule-high",
                name="VIP问候",
                response_type="fixed",
                fixed_reply="VIP问候回复",
                keywords=["你好"],
                priority=10,
            ),
        ]

        result = self.router.match("你好", rules)

        assert result is not None
        assert result.rule.id == "rule-high"
        assert result.rule.fixed_reply == "VIP问候回复"

    def test_no_match_returns_none(self):
        """No matching intent should return None."""
        rules = [
            MockIntentRule(
                id="rule-1",
                name="退货",
                response_type="rag",
                keywords=["退货"],
                priority=10,
            )
        ]

        result = self.router.match("今天天气怎么样", rules)

        assert result is None


class TestMetadataFilterInjection:
    """
    [AC-IDSMETA-19] Test metadata filter injection in RAG retrieval.
    """

    @pytest.mark.asyncio
    async def test_filter_injection_with_grade_subject_scene(self):
        """RAG retrieval should inject grade/subject/scene metadata filters."""
        retriever = MockRetriever(hits=[
            MockRetrievalHit(
                text="初一数学知识点",
                score=0.9,
                source="kb",
                metadata={"grade": "初一", "subject": "数学", "scene": "课后辅导", "kb_id": "kb-1"},
            ),
            MockRetrievalHit(
                text="初二物理知识点",
                score=0.85,
                source="kb",
                metadata={"grade": "初二", "subject": "物理", "scene": "课后辅导", "kb_id": "kb-1"},
            ),
        ])

        metadata_filter = {
            "grade": "初一",
            "subject": "数学",
            "scene": "课后辅导",
        }

        result = await retriever.retrieve(
            tenant_id="tenant-1",
            query="数学知识点",
            metadata_filter=metadata_filter,
        )

        assert retriever._last_filter == metadata_filter
        assert result.hit_count == 1
        assert result.hits[0].metadata["grade"] == "初一"

    @pytest.mark.asyncio
    async def test_target_kb_ids_filtering(self):
        """RAG retrieval should filter by target KB IDs from intent."""
        retriever = MockRetriever(hits=[
            MockRetrievalHit(
                text="退货政策",
                score=0.9,
                source="kb",
                metadata={"kb_id": "kb-return"},
            ),
            MockRetrievalHit(
                text="产品介绍",
                score=0.85,
                source="kb",
                metadata={"kb_id": "kb-product"},
            ),
        ])

        result = await retriever.retrieve(
            tenant_id="tenant-1",
            query="退货",
            target_kb_ids=["kb-return"],
        )

        assert retriever._last_target_kb_ids == ["kb-return"]
        assert result.hit_count == 1
        assert result.hits[0].metadata["kb_id"] == "kb-return"

    @pytest.mark.asyncio
    async def test_combined_filters(self):
        """RAG retrieval should combine target KB and metadata filters."""
        retriever = MockRetriever(hits=[
            MockRetrievalHit(
                text="初一数学教材",
                score=0.9,
                source="kb",
                metadata={"grade": "初一", "subject": "数学", "kb_id": "kb-edu"},
            ),
            MockRetrievalHit(
                text="初二数学教材",
                score=0.85,
                source="kb",
                metadata={"grade": "初二", "subject": "数学", "kb_id": "kb-edu"},
            ),
            MockRetrievalHit(
                text="初一数学练习",
                score=0.8,
                source="kb",
                metadata={"grade": "初一", "subject": "数学", "kb_id": "kb-exercise"},
            ),
        ])

        result = await retriever.retrieve(
            tenant_id="tenant-1",
            query="数学",
            target_kb_ids=["kb-edu"],
            metadata_filter={"grade": "初一"},
        )

        assert result.hit_count == 1
        assert result.hits[0].metadata["grade"] == "初一"
        assert result.hits[0].metadata["kb_id"] == "kb-edu"


class TestFallbackStrategy:
    """
    [AC-IDSMETA-20] Test fallback strategy with structured reason codes.
    """

    def setup_method(self):
        self.fallback = FallbackStrategy()

    def test_no_intent_match_fallback(self):
        """No intent match should trigger fallback with reason code."""
        result = self.fallback.execute(
            reason="NO_INTENT_MATCH",
            fallback_message="抱歉，我不太理解您的问题，请换种方式描述。",
        )

        assert result["fallback_triggered"] is True
        assert result["reason_code"] == "intent_not_matched"
        assert result["fallback_type"] == "fixed"
        assert "不太理解" in result["fallback_content"]

    def test_no_retrieval_hits_fallback(self):
        """No retrieval hits should trigger fallback with reason code."""
        result = self.fallback.execute(
            reason="NO_RETRIEVAL_HITS",
            fallback_kb_id="kb-general",
        )

        assert result["fallback_triggered"] is True
        assert result["reason_code"] == "retrieval_empty"
        assert result["fallback_type"] == "kb"
        assert result["fallback_kb_id"] == "kb-general"

    def test_low_confidence_fallback(self):
        """Low confidence should trigger fallback with reason code."""
        result = self.fallback.execute(
            reason="LOW_CONFIDENCE",
            fallback_message="我对这个回答不太确定，建议您咨询人工客服。",
        )

        assert result["fallback_triggered"] is True
        assert result["reason_code"] == "confidence_below_threshold"
        assert result["fallback_type"] == "fixed"

    def test_metadata_filter_too_strict_fallback(self):
        """Too strict metadata filter should trigger fallback."""
        result = self.fallback.execute(
            reason="METADATA_FILTER_TOO_STRICT",
            fallback_message="没有找到符合条件的答案，请尝试调整筛选条件。",
        )

        assert result["fallback_triggered"] is True
        assert result["reason_code"] == "filter_excluded_all"

    def test_default_fallback(self):
        """Default fallback should be used when no specific fallback provided."""
        result = self.fallback.execute(reason="NO_RETRIEVAL_HITS")

        assert result["fallback_triggered"] is True
        assert result["fallback_type"] == "default"
        assert "人工客服" in result["fallback_content"]


class TestRoutingFilterRetrievalFallbackChain:
    """
    [AC-IDSMETA-18, AC-IDSMETA-19, AC-IDSMETA-20] Test complete chain.
    """

    @pytest.mark.asyncio
    async def test_full_chain_with_intent_match_and_retrieval(self):
        """Full chain: intent match -> metadata filter -> retrieval -> response."""
        router = MockIntentRouter()
        retriever = MockRetriever(hits=[
            MockRetrievalHit(
                text="退货需在7天内，商品未拆封",
                score=0.9,
                source="kb",
                metadata={"kb_id": "kb-return"},
            ),
        ])
        fallback = FallbackStrategy()

        rules = [
            MockIntentRule(
                id="rule-1",
                name="退货",
                response_type="rag",
                target_kb_ids=["kb-return"],
                keywords=["退货"],
                priority=10,
            )
        ]

        user_message = "我想退货"
        intent_result = router.match(user_message, rules)

        assert intent_result is not None
        assert intent_result.rule.response_type == "rag"

        retrieval_result = await retriever.retrieve(
            tenant_id="tenant-1",
            query=user_message,
            target_kb_ids=intent_result.rule.target_kb_ids,
        )

        assert retrieval_result.hit_count > 0
        assert not retrieval_result.is_empty

    @pytest.mark.asyncio
    async def test_full_chain_no_intent_match_fallback(self):
        """Full chain: no intent match -> fallback."""
        router = MockIntentRouter()
        fallback = FallbackStrategy()

        rules = [
            MockIntentRule(
                id="rule-1",
                name="退货",
                response_type="rag",
                keywords=["退货"],
                priority=10,
            )
        ]

        user_message = "今天天气怎么样"
        intent_result = router.match(user_message, rules)

        assert intent_result is None

        fallback_result = fallback.execute(
            reason="NO_INTENT_MATCH",
            fallback_message="抱歉，我无法回答这个问题。",
        )

        assert fallback_result["fallback_triggered"] is True
        assert fallback_result["reason_code"] == "intent_not_matched"

    @pytest.mark.asyncio
    async def test_full_chain_no_retrieval_hits_fallback(self):
        """Full chain: intent match -> no retrieval hits -> fallback."""
        router = MockIntentRouter()
        retriever = MockRetriever(hits=[])
        fallback = FallbackStrategy()

        rules = [
            MockIntentRule(
                id="rule-1",
                name="退货",
                response_type="rag",
                target_kb_ids=["kb-return"],
                keywords=["退货"],
                priority=10,
            )
        ]

        user_message = "退货流程是什么"
        intent_result = router.match(user_message, rules)

        assert intent_result is not None

        retrieval_result = await retriever.retrieve(
            tenant_id="tenant-1",
            query=user_message,
            target_kb_ids=intent_result.rule.target_kb_ids,
        )

        assert retrieval_result.is_empty

        fallback_result = fallback.execute(
            reason="NO_RETRIEVAL_HITS",
            fallback_kb_id="kb-general",
        )

        assert fallback_result["fallback_triggered"] is True
        assert fallback_result["reason_code"] == "retrieval_empty"

    @pytest.mark.asyncio
    async def test_full_chain_with_metadata_filter(self):
        """Full chain with metadata filter injection."""
        router = MockIntentRouter()
        retriever = MockRetriever(hits=[
            MockRetrievalHit(
                text="初一数学课程大纲",
                score=0.9,
                source="kb",
                metadata={"grade": "初一", "subject": "数学", "scene": "咨询", "kb_id": "kb-edu"},
            ),
            MockRetrievalHit(
                text="初二数学课程大纲",
                score=0.85,
                source="kb",
                metadata={"grade": "初二", "subject": "数学", "scene": "咨询", "kb_id": "kb-edu"},
            ),
        ])
        fallback = FallbackStrategy()

        rules = [
            MockIntentRule(
                id="rule-1",
                name="课程咨询",
                response_type="rag",
                target_kb_ids=["kb-edu"],
                keywords=["课程", "大纲"],
                priority=10,
            )
        ]

        user_message = "初一数学课程大纲"
        session_metadata = {"grade": "初一", "subject": "数学", "scene": "咨询"}

        intent_result = router.match(user_message, rules)

        retrieval_result = await retriever.retrieve(
            tenant_id="tenant-1",
            query=user_message,
            target_kb_ids=intent_result.rule.target_kb_ids if intent_result else None,
            metadata_filter=session_metadata,
        )

        assert retrieval_result.hit_count == 1
        assert retrieval_result.hits[0].metadata["grade"] == "初一"


class TestReasonCodeStructure:
    """
    [AC-IDSMETA-20] Test structured reason codes for fallback.
    """

    def test_reason_code_format(self):
        """Reason codes should follow snake_case format."""
        fallback = FallbackStrategy()

        for reason_key, expected_code in FallbackStrategy.REASON_CODES.items():
            result = fallback.execute(reason=reason_key)
            assert result["reason_code"] == expected_code
            assert "_" in expected_code or expected_code.islower()

    def test_reason_code_in_diagnostics(self):
        """Reason code should be included in diagnostics."""
        fallback = FallbackStrategy()

        result = fallback.execute(reason="NO_RETRIEVAL_HITS")

        assert "reason_code" in result
        assert result["reason_code"] == "retrieval_empty"

    def test_unknown_reason_code(self):
        """Unknown reason should return 'unknown' code."""
        fallback = FallbackStrategy()

        result = fallback.execute(reason="UNKNOWN_REASON")

        assert result["reason_code"] == "unknown"
