"""
Tests for Mid Platform Memory and Tool Governance services.
[AC-IDMP-13/14/15/19] 记忆与工具治理测试

覆盖路径：
- 成功路径：正常 recall/update，工具调用成功
- 超时路径：recall 超时降级，工具调用超时
- 错误路径：recall 失败降级，工具调用错误
- 降级路径：记忆服务不可用时继续主链路
"""

import asyncio
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from datetime import datetime

from app.services.mid.memory_adapter import MemoryAdapter, UserMemory
from app.services.mid.tool_call_recorder import ToolCallRecorder, ToolCallStatistics, get_tool_call_recorder
from app.services.mid.tool_registry import ToolRegistry, ToolDefinition, ToolExecutionResult, get_tool_registry, init_tool_registry
from app.models.mid.memory import (
    RecallRequest,
    RecallResponse,
    UpdateRequest,
    MemoryProfile,
    MemoryFact,
    MemoryPreferences,
)
from app.models.mid.tool_trace import (
    ToolCallTrace,
    ToolCallBuilder,
    ToolCallStatus,
    ToolType,
)
from app.models.mid.schemas import ToolCallTrace as ToolCallTraceSchema


class TestMemoryAdapter:
    """[AC-IDMP-13/14] 记忆适配器测试"""

    @pytest.fixture
    def mock_session(self):
        return AsyncMock()

    @pytest.fixture
    def adapter(self, mock_session):
        return MemoryAdapter(mock_session)

    @pytest.mark.asyncio
    async def test_recall_success(self, adapter):
        """[AC-IDMP-13] 成功召回用户记忆"""
        response = await adapter.recall(
            user_id="user123",
            session_id="session456",
            tenant_id="tenant789",
        )
        
        assert response is not None
        assert response.profile is not None
        assert response.profile.grade == "初一"
        assert response.profile.region == "北京"
        assert len(response.facts) > 0
        assert response.preferences is not None
        assert response.preferences.tone == "friendly"

    @pytest.mark.asyncio
    async def test_recall_returns_context_for_prompt(self, adapter):
        """[AC-IDMP-13] 召回记忆可注入 Prompt"""
        response = await adapter.recall(
            user_id="user123",
            session_id="session456",
        )
        
        context = response.get_context_for_prompt()
        
        assert "【用户属性】" in context
        assert "年级" in context
        assert "【已知事实】" in context
        assert "【用户偏好】" in context

    @pytest.mark.asyncio
    async def test_recall_timeout_fallback(self, mock_session):
        """[AC-IDMP-13] recall 超时降级，不阻断主链路"""
        adapter = MemoryAdapter(mock_session, recall_timeout_ms=1)
        
        async def slow_recall(*args, **kwargs):
            await asyncio.sleep(1)
            return RecallResponse()
        
        with patch.object(adapter, '_recall_internal', slow_recall):
            response = await adapter.recall(
                user_id="user123",
                session_id="session456",
            )
        
        assert response is not None
        assert response.profile is None
        assert response.facts == []

    @pytest.mark.asyncio
    async def test_recall_error_fallback(self, adapter, mock_session):
        """[AC-IDMP-13] recall 错误降级，不阻断主链路"""
        with patch.object(adapter, '_recall_internal', side_effect=Exception("DB error")):
            response = await adapter.recall(
                user_id="user123",
                session_id="session456",
            )
        
        assert response is not None
        assert response.profile is None

    @pytest.mark.asyncio
    async def test_update_async(self, adapter):
        """[AC-IDMP-14] 异步更新用户记忆"""
        result = await adapter.update(
            user_id="user123",
            session_id="session456",
            messages=[{"role": "user", "content": "你好"}],
            summary="用户打招呼",
        )
        
        assert result is True
        
        await asyncio.sleep(0.1)
        
        completed = await adapter.wait_pending_updates(timeout=1.0)
        assert completed >= 0

    @pytest.mark.asyncio
    async def test_update_with_summary_generation(self, adapter):
        """[AC-IDMP-14] 带摘要生成的记忆更新"""
        async def summary_gen(messages):
            return "这是一个测试摘要"
        
        result = await adapter.update_with_summary_generation(
            user_id="user123",
            session_id="session456",
            messages=[{"role": "user", "content": "你好"}],
            summary_generator=summary_gen,
        )
        
        assert result is True


class TestToolCallRecorder:
    """[AC-IDMP-15] 工具调用记录器测试"""

    @pytest.fixture
    def recorder(self):
        return ToolCallRecorder()

    def test_record_success(self, recorder):
        """[AC-IDMP-15] 记录成功的工具调用"""
        trace = recorder.record_success(
            session_id="session123",
            tool_name="kb_search",
            tool_type=ToolType.INTERNAL,
            duration_ms=150,
            args={"query": "测试查询"},
            result={"docs": ["doc1", "doc2"]},
            registry_version="1.0.0",
            auth_applied=False,
        )
        
        assert trace.tool_name == "kb_search"
        assert trace.status == ToolCallStatus.OK
        assert trace.duration_ms == 150
        assert trace.args_digest is not None
        assert trace.result_digest is not None

    def test_record_timeout(self, recorder):
        """[AC-IDMP-15] 记录超时的工具调用"""
        trace = recorder.record_timeout(
            session_id="session123",
            tool_name="kb_search",
            tool_type=ToolType.INTERNAL,
            duration_ms=2000,
            args={"query": "测试查询"},
        )
        
        assert trace.status == ToolCallStatus.TIMEOUT
        assert trace.error_code == "TIMEOUT"

    def test_record_error(self, recorder):
        """[AC-IDMP-15] 记录错误的工具调用"""
        trace = recorder.record_error(
            session_id="session123",
            tool_name="kb_search",
            tool_type=ToolType.INTERNAL,
            duration_ms=500,
            error_code="DB_CONNECTION_ERROR",
            error_message="Database connection failed",
        )
        
        assert trace.status == ToolCallStatus.ERROR
        assert trace.error_code == "DB_CONNECTION_ERROR"

    def test_record_rejected(self, recorder):
        """[AC-IDMP-15] 记录被拒绝的工具调用"""
        trace = recorder.record_rejected(
            session_id="session123",
            tool_name="sensitive_tool",
            tool_type=ToolType.MCP,
            reason="AUTH_REQUIRED",
        )
        
        assert trace.status == ToolCallStatus.REJECTED
        assert trace.error_code == "AUTH_REQUIRED"

    def test_get_traces(self, recorder):
        """[AC-IDMP-15] 获取会话的工具调用记录"""
        recorder.record_success(
            session_id="session123",
            tool_name="tool1",
            tool_type=ToolType.INTERNAL,
            duration_ms=100,
        )
        recorder.record_success(
            session_id="session123",
            tool_name="tool2",
            tool_type=ToolType.INTERNAL,
            duration_ms=200,
        )
        
        traces = recorder.get_traces("session123")
        
        assert len(traces) == 2
        assert traces[0].tool_name == "tool1"
        assert traces[1].tool_name == "tool2"

    def test_get_statistics(self, recorder):
        """[AC-IDMP-15] 获取工具调用统计"""
        recorder.record_success(
            session_id="session123",
            tool_name="kb_search",
            tool_type=ToolType.INTERNAL,
            duration_ms=100,
        )
        recorder.record_timeout(
            session_id="session123",
            tool_name="kb_search",
            tool_type=ToolType.INTERNAL,
            duration_ms=2000,
        )
        
        stats = recorder.get_statistics("kb_search")
        
        assert stats["total_calls"] == 2
        assert stats["success_rate"] == 0.5
        assert stats["timeout_rate"] == 0.5

    def test_to_trace_info_format(self, recorder):
        """[AC-IDMP-15] 转换为 TraceInfo 格式"""
        recorder.record_success(
            session_id="session123",
            tool_name="kb_search",
            tool_type=ToolType.INTERNAL,
            duration_ms=100,
        )
        
        traces = recorder.to_trace_info_format("session123")
        
        assert len(traces) == 1
        assert traces[0]["tool_name"] == "kb_search"
        assert traces[0]["status"] == "ok"

    def test_compute_digest(self, recorder):
        """[AC-IDMP-15] 敏感参数脱敏"""
        long_data = {"query": "x" * 100}
        digest = ToolCallTrace.compute_digest(long_data)
        
        assert len(digest) < 100
        assert "..." in digest or len(digest) <= 64


class TestToolRegistry:
    """[AC-IDMP-19] Tool Registry 治理测试"""

    @pytest.fixture
    def registry(self):
        return ToolRegistry()

    def test_register_tool(self, registry):
        """[AC-IDMP-19] 注册工具"""
        async def dummy_handler(**kwargs):
            return {"result": "ok"}
        
        tool = registry.register(
            name="test_tool",
            description="测试工具",
            handler=dummy_handler,
            tool_type=ToolType.INTERNAL,
            version="1.0.0",
            auth_required=False,
            timeout_ms=1000,
        )
        
        assert tool.name == "test_tool"
        assert tool.enabled is True
        assert tool.timeout_ms == 1000

    def test_register_mcp_tool(self, registry):
        """[AC-IDMP-19] 注册 MCP 工具"""
        async def mcp_handler(**kwargs):
            return {"result": "ok"}
        
        tool = registry.register(
            name="mcp_tool",
            description="MCP 工具",
            handler=mcp_handler,
            tool_type=ToolType.MCP,
        )
        
        assert tool.tool_type == ToolType.MCP

    def test_enable_disable_tool(self, registry):
        """[AC-IDMP-19] 启停工具"""
        async def handler(**kwargs):
            return {}
        
        registry.register(
            name="toggle_tool",
            description="可切换工具",
            handler=handler,
        )
        
        assert registry.get_tool("toggle_tool").enabled is True
        
        registry.disable_tool("toggle_tool")
        assert registry.get_tool("toggle_tool").enabled is False
        
        registry.enable_tool("toggle_tool")
        assert registry.get_tool("toggle_tool").enabled is True

    @pytest.mark.asyncio
    async def test_execute_tool_success(self, registry):
        """[AC-IDMP-19] 执行工具成功"""
        async def handler(**kwargs):
            return {"data": kwargs.get("query")}
        
        registry.register(
            name="search",
            description="搜索工具",
            handler=handler,
            timeout_ms=1000,
        )
        
        result = await registry.execute(
            tool_name="search",
            args={"query": "test"},
        )
        
        assert result.success is True
        assert result.output["data"] == "test"

    @pytest.mark.asyncio
    async def test_execute_tool_timeout(self, registry):
        """[AC-IDMP-19] 工具执行超时"""
        async def slow_handler(**kwargs):
            await asyncio.sleep(5)
            return {"data": "ok"}
        
        registry.register(
            name="slow_tool",
            description="慢工具",
            handler=slow_handler,
            timeout_ms=100,
        )
        
        result = await registry.execute(
            tool_name="slow_tool",
            args={},
        )
        
        assert result.success is False
        assert "timeout" in result.error.lower()

    @pytest.mark.asyncio
    async def test_execute_tool_not_found(self, registry):
        """[AC-IDMP-19] 工具不存在"""
        result = await registry.execute(
            tool_name="nonexistent",
            args={},
        )
        
        assert result.success is False
        assert "not found" in result.error.lower()

    @pytest.mark.asyncio
    async def test_execute_tool_disabled(self, registry):
        """[AC-IDMP-19] 工具已禁用"""
        async def handler(**kwargs):
            return {}
        
        registry.register(
            name="disabled_tool",
            description="禁用工具",
            handler=handler,
            enabled=False,
        )
        
        result = await registry.execute(
            tool_name="disabled_tool",
            args={},
        )
        
        assert result.success is False
        assert "disabled" in result.error.lower()

    @pytest.mark.asyncio
    async def test_execute_tool_auth_required(self, registry):
        """[AC-IDMP-19] 工具需要鉴权"""
        async def handler(**kwargs):
            return {}
        
        registry.register(
            name="auth_tool",
            description="需要鉴权的工具",
            handler=handler,
            auth_required=True,
        )
        
        result = await registry.execute(
            tool_name="auth_tool",
            args={},
            auth_context=None,
        )
        
        assert result.success is False
        assert "auth" in result.error.lower()

    @pytest.mark.asyncio
    async def test_execute_tool_with_auth_context(self, registry):
        """[AC-IDMP-19] 带鉴权上下文执行工具"""
        async def handler(**kwargs):
            return {"authenticated": True}
        
        registry.register(
            name="auth_tool2",
            description="需要鉴权的工具",
            handler=handler,
            auth_required=True,
        )
        
        result = await registry.execute(
            tool_name="auth_tool2",
            args={},
            auth_context={"user_id": "user123"},
        )
        
        assert result.success is True
        assert result.auth_applied is True

    def test_create_trace(self, registry):
        """[AC-IDMP-19] 创建工具调用追踪"""
        result = ToolExecutionResult(
            success=True,
            output={"data": "ok"},
            duration_ms=100,
            auth_applied=False,
            registry_version="1.0.0",
        )
        
        trace = registry.create_trace(
            tool_name="test_tool",
            result=result,
            args_digest="query=test",
        )
        
        assert trace.tool_name == "test_tool"
        assert trace.status == ToolCallStatus.OK

    def test_get_governance_report(self, registry):
        """[AC-IDMP-19] 获取治理报告"""
        async def handler(**kwargs):
            return {}
        
        registry.register(name="tool1", description="工具1", handler=handler)
        registry.register(name="tool2", description="工具2", handler=handler, enabled=False)
        registry.register(name="tool3", description="工具3", handler=handler, auth_required=True)
        
        report = registry.get_governance_report()
        
        assert report["total_tools"] == 3
        assert report["enabled_tools"] == 2
        assert report["disabled_tools"] == 1
        assert report["auth_required_tools"] == 1

    def test_get_tool_registry_singleton(self):
        """[AC-IDMP-19] 获取全局注册表实例"""
        registry1 = get_tool_registry()
        registry2 = get_tool_registry()
        
        assert registry1 is registry2

    def test_init_tool_registry(self):
        """[AC-IDMP-19] 初始化注册表"""
        from app.services.mid.timeout_governor import TimeoutGovernor
        
        governor = TimeoutGovernor()
        registry = init_tool_registry(timeout_governor=governor)
        
        assert registry is not None
        assert registry._timeout_governor is governor


class TestToolCallBuilder:
    """[AC-IDMP-15] 工具调用构建器测试"""

    def test_build_success_trace(self):
        """[AC-IDMP-15] 构建成功追踪"""
        builder = ToolCallBuilder(
            tool_name="test_tool",
            tool_type=ToolType.INTERNAL,
        )
        
        trace = builder.with_args({"query": "test"}).with_result({"data": "ok"}).build()
        
        assert trace.tool_name == "test_tool"
        assert trace.status == ToolCallStatus.OK
        assert trace.args_digest is not None
        assert trace.result_digest is not None

    def test_build_error_trace(self):
        """[AC-IDMP-15] 构建错误追踪"""
        builder = ToolCallBuilder(tool_name="test_tool")
        
        trace = builder.with_error(
            Exception("Something went wrong"),
            error_code="INTERNAL_ERROR",
        ).build()
        
        assert trace.status == ToolCallStatus.ERROR
        assert trace.error_code == "INTERNAL_ERROR"

    def test_build_timeout_trace(self):
        """[AC-IDMP-15] 构建超时追踪"""
        builder = ToolCallBuilder(tool_name="test_tool")
        
        trace = builder.with_error(TimeoutError("Timeout")).build()
        
        assert trace.status == ToolCallStatus.TIMEOUT

    def test_build_rejected_trace(self):
        """[AC-IDMP-15] 构建拒绝追踪"""
        builder = ToolCallBuilder(tool_name="test_tool")
        
        trace = builder.with_rejected("AUTH_DENIED").build()
        
        assert trace.status == ToolCallStatus.REJECTED
        assert trace.error_code == "AUTH_DENIED"
