"""
Tests for Scene Slot Bundle Loader.
[AC-SCENE-SLOT-02] 场景槽位包加载器测试
"""

import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from uuid import uuid4

from app.services.mid.scene_slot_bundle_loader import (
    SceneSlotBundleLoader,
    SceneSlotContext,
    SlotInfo,
)
from app.models.entities import (
    SceneSlotBundle,
    SceneSlotBundleStatus,
    SlotDefinition,
)


@pytest.fixture
def mock_session():
    """Mock database session."""
    session = AsyncMock()
    session.execute = AsyncMock()
    return session


@pytest.fixture
def sample_slot_definitions():
    """Sample slot definitions for testing."""
    return [
        SlotDefinition(
            id=uuid4(),
            tenant_id="test_tenant",
            slot_key="course_type",
            type="string",
            required=True,
            ask_back_prompt="请问您想咨询哪种类型的课程？",
        ),
        SlotDefinition(
            id=uuid4(),
            tenant_id="test_tenant",
            slot_key="grade",
            type="string",
            required=True,
            ask_back_prompt="请问您是几年级？",
        ),
        SlotDefinition(
            id=uuid4(),
            tenant_id="test_tenant",
            slot_key="region",
            type="string",
            required=False,
            ask_back_prompt="请问您在哪个地区？",
        ),
    ]


@pytest.fixture
def sample_bundle(sample_slot_definitions):
    """Sample scene slot bundle for testing."""
    return SceneSlotBundle(
        id=uuid4(),
        tenant_id="test_tenant",
        scene_key="open_consult",
        scene_name="开放咨询",
        description="开放咨询场景的槽位配置",
        required_slots=["course_type", "grade"],
        optional_slots=["region"],
        slot_priority=["course_type", "grade", "region"],
        completion_threshold=1.0,
        ask_back_order="priority",
        status=SceneSlotBundleStatus.ACTIVE.value,
        version=1,
    )


class TestSceneSlotContext:
    """Test cases for SceneSlotContext."""
    
    def test_get_all_slot_keys(self):
        """Test getting all slot keys."""
        context = SceneSlotContext(
            scene_key="test_scene",
            scene_name="测试场景",
            required_slots=[
                SlotInfo(slot_key="course_type", type="string", required=True),
            ],
            optional_slots=[
                SlotInfo(slot_key="region", type="string", required=False),
            ],
        )
        
        all_keys = context.get_all_slot_keys()
        
        assert "course_type" in all_keys
        assert "region" in all_keys
        assert len(all_keys) == 2
    
    def test_get_missing_slots(self):
        """Test getting missing slots."""
        context = SceneSlotContext(
            scene_key="test_scene",
            scene_name="测试场景",
            required_slots=[
                SlotInfo(slot_key="course_type", type="string", required=True, ask_back_prompt="请问课程类型？"),
                SlotInfo(slot_key="grade", type="string", required=True, ask_back_prompt="请问年级？"),
            ],
            optional_slots=[],
        )
        
        filled_slots = {"course_type": "数学"}
        missing = context.get_missing_slots(filled_slots)
        
        assert len(missing) == 1
        assert missing[0]["slot_key"] == "grade"
    
    def test_get_ordered_missing_slots_priority(self):
        """Test getting ordered missing slots with priority order."""
        context = SceneSlotContext(
            scene_key="test_scene",
            scene_name="测试场景",
            required_slots=[
                SlotInfo(slot_key="course_type", type="string", required=True),
                SlotInfo(slot_key="grade", type="string", required=True),
            ],
            optional_slots=[],
            slot_priority=["grade", "course_type"],
            ask_back_order="priority",
        )
        
        filled_slots = {}
        missing = context.get_ordered_missing_slots(filled_slots)
        
        assert len(missing) == 2
        assert missing[0]["slot_key"] == "grade"
        assert missing[1]["slot_key"] == "course_type"
    
    def test_get_completion_ratio(self):
        """Test calculating completion ratio."""
        context = SceneSlotContext(
            scene_key="test_scene",
            scene_name="测试场景",
            required_slots=[
                SlotInfo(slot_key="course_type", type="string", required=True),
                SlotInfo(slot_key="grade", type="string", required=True),
            ],
            optional_slots=[],
            completion_threshold=0.5,
        )
        
        filled_slots = {"course_type": "数学"}
        ratio = context.get_completion_ratio(filled_slots)
        
        assert ratio == 0.5
    
    def test_is_complete(self):
        """Test checking if complete."""
        context = SceneSlotContext(
            scene_key="test_scene",
            scene_name="测试场景",
            required_slots=[
                SlotInfo(slot_key="course_type", type="string", required=True),
                SlotInfo(slot_key="grade", type="string", required=True),
            ],
            optional_slots=[],
            completion_threshold=1.0,
        )
        
        filled_slots = {"course_type": "数学", "grade": "高一"}
        is_complete = context.is_complete(filled_slots)
        
        assert is_complete is True
        
        filled_slots_partial = {"course_type": "数学"}
        is_complete_partial = context.is_complete(filled_slots_partial)
        
        assert is_complete_partial is False


class TestSceneSlotBundleLoader:
    """Test cases for SceneSlotBundleLoader."""
    
    @pytest.mark.asyncio
    async def test_load_scene_context(self, mock_session, sample_bundle, sample_slot_definitions):
        """Test loading scene context."""
        loader = SceneSlotBundleLoader(mock_session)
        
        with patch.object(loader._bundle_service, 'get_active_bundle_by_scene', new_callable=AsyncMock) as mock_get_bundle:
            mock_get_bundle.return_value = sample_bundle
            
            with patch.object(loader._slot_service, 'list_slot_definitions', new_callable=AsyncMock) as mock_get_slots:
                mock_get_slots.return_value = sample_slot_definitions
                
                context = await loader.load_scene_context("test_tenant", "open_consult")
                
                assert context is not None
                assert context.scene_key == "open_consult"
                assert len(context.required_slots) == 2
                assert len(context.optional_slots) == 1
    
    @pytest.mark.asyncio
    async def test_load_scene_context_not_found(self, mock_session):
        """Test loading scene context when bundle not found."""
        loader = SceneSlotBundleLoader(mock_session)
        
        with patch.object(loader._bundle_service, 'get_active_bundle_by_scene', new_callable=AsyncMock) as mock_get:
            mock_get.return_value = None
            
            context = await loader.load_scene_context("test_tenant", "unknown_scene")
            
            assert context is None
    
    @pytest.mark.asyncio
    async def test_get_missing_slots_for_scene(self, mock_session, sample_bundle, sample_slot_definitions):
        """Test getting missing slots for a scene."""
        loader = SceneSlotBundleLoader(mock_session)
        
        with patch.object(loader, 'load_scene_context', new_callable=AsyncMock) as mock_load:
            mock_context = SceneSlotContext(
                scene_key="open_consult",
                scene_name="开放咨询",
                required_slots=[
                    SlotInfo(slot_key="course_type", type="string", required=True, ask_back_prompt="请问课程类型？"),
                    SlotInfo(slot_key="grade", type="string", required=True, ask_back_prompt="请问年级？"),
                ],
                optional_slots=[],
                slot_priority=["course_type", "grade"],
            )
            mock_load.return_value = mock_context
            
            filled_slots = {"course_type": "数学"}
            missing = await loader.get_missing_slots_for_scene(
                "test_tenant", "open_consult", filled_slots
            )
            
            assert len(missing) == 1
            assert missing[0]["slot_key"] == "grade"
    
    @pytest.mark.asyncio
    async def test_generate_ask_back_prompt_single(self, mock_session):
        """Test generating ask-back prompt for single missing slot."""
        loader = SceneSlotBundleLoader(mock_session)
        
        with patch.object(loader, 'load_scene_context', new_callable=AsyncMock) as mock_load:
            mock_context = SceneSlotContext(
                scene_key="open_consult",
                scene_name="开放咨询",
                required_slots=[
                    SlotInfo(slot_key="course_type", type="string", required=True, ask_back_prompt="请问您想咨询哪种课程？"),
                ],
                optional_slots=[],
                ask_back_order="priority",
            )
            mock_load.return_value = mock_context
            
            with patch.object(loader, 'get_missing_slots_for_scene', new_callable=AsyncMock) as mock_missing:
                mock_missing.return_value = [
                    {"slot_key": "course_type", "ask_back_prompt": "请问您想咨询哪种课程？"}
                ]
                
                prompt = await loader.generate_ask_back_prompt(
                    "test_tenant", "open_consult", {}
                )
                
                assert prompt == "请问您想咨询哪种课程？"
    
    @pytest.mark.asyncio
    async def test_generate_ask_back_prompt_parallel(self, mock_session):
        """Test generating ask-back prompt with parallel strategy."""
        loader = SceneSlotBundleLoader(mock_session)
        
        with patch.object(loader, 'load_scene_context', new_callable=AsyncMock) as mock_load:
            mock_context = SceneSlotContext(
                scene_key="open_consult",
                scene_name="开放咨询",
                required_slots=[
                    SlotInfo(slot_key="course_type", type="string", required=True, ask_back_prompt="课程类型"),
                    SlotInfo(slot_key="grade", type="string", required=True, ask_back_prompt="年级"),
                ],
                optional_slots=[],
                ask_back_order="parallel",
            )
            mock_load.return_value = mock_context
            
            with patch.object(loader, 'get_missing_slots_for_scene', new_callable=AsyncMock) as mock_missing:
                mock_missing.return_value = [
                    {"slot_key": "course_type", "ask_back_prompt": "课程类型"},
                    {"slot_key": "grade", "ask_back_prompt": "年级"},
                ]
                
                prompt = await loader.generate_ask_back_prompt(
                    "test_tenant", "open_consult", {}
                )
                
                assert "课程类型" in prompt
                assert "年级" in prompt
