"""
搜索服务单元测试

测试语义检索、关键词检索和混合检索功能。
"""

import pytest
import sys
import os

# 将项目根目录添加到 sys.path
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
if project_root not in sys.path:
    sys.path.insert(0, project_root)

from unittest.mock import Mock, MagicMock, patch
from src.services.search_service import SearchService
from src.database.models import QuestionSearchFilter


class TestSearchService:
    """搜索服务测试类"""

    @pytest.fixture
    def mock_db_manager(self):
        """模拟数据库管理器"""
        db_manager = Mock()
        db_manager.chroma_dao = Mock()
        db_manager.sqlite_dao = Mock()
        db_manager.get_statistics = Mock(return_value={"total_questions": 100})
        return db_manager

    @pytest.fixture
    def mock_embedding_service(self):
        """模拟Embedding服务"""
        embedding_service = Mock()
        embedding_service.embed_text = Mock(return_value=[0.1] * 1536)
        embedding_service.get_cache_stats = Mock(return_value={"hits": 100, "misses": 10})
        return embedding_service

    @pytest.fixture
    def search_service(self, mock_db_manager, mock_embedding_service):
        """创建搜索服务实例"""
        return SearchService(
            db_manager=mock_db_manager,
            embedding_service=mock_embedding_service,
            logger=Mock()
        )

    def test_search_by_semantic_success(self, search_service, mock_db_manager):
        """测试语义检索成功"""
        # 准备模拟数据
        mock_question = {
            'question_id': 'q1',
            'title': '测试题目',
            'content': '这是测试内容',
            'question_type': '单选',
            'category': '数学',
            'difficulty': '中等',
            'tags': ['标签1', '标签2'],
            'answer': '答案A',
            'explanation': '解析内容',
            'status': '已发布',
            'created_at': '2024-01-01',
            'updated_at': '2024-01-01'
        }

        mock_db_manager.chroma_dao.search_similar.return_value = [
            {'question_id': 'q1', 'similarity_score': 0.95}
        ]
        mock_db_manager.sqlite_dao.get_question.return_value = mock_question

        # 执行检索
        result = search_service.search_by_semantic(
            query='测试查询',
            top_k=10,
            include_metadata=True
        )

        # 验证结果
        assert result['query'] == '测试查询'
        assert result['search_type'] == 'semantic'
        assert result['total_results'] == 1
        assert len(result['results']) == 1
        assert result['results'][0]['question_id'] == 'q1'
        assert result['results'][0]['search_score'] == 0.95
        assert result['results'][0]['search_type'] == 'semantic'

        # 验证调用
        mock_db_manager.chroma_dao.search_similar.assert_called_once()
        mock_db_manager.sqlite_dao.get_question.assert_called_once()

    def test_search_by_semantic_with_filters(self, search_service, mock_db_manager):
        """测试带过滤条件的语义检索"""
        # 准备模拟数据
        mock_db_manager.chroma_dao.search_similar.return_value = []

        # 准备过滤条件
        filters = QuestionSearchFilter(
            category='数学',
            difficulty='中等'
        )

        # 执行检索
        result = search_service.search_by_semantic(
            query='测试查询',
            top_k=10,
            filters=filters
        )

        # 验证过滤条件被正确传递
        call_args = mock_db_manager.chroma_dao.search_similar.call_args
        assert call_args[1]['where'] is not None
        assert call_args[1]['where']['category'] == '数学'
        assert call_args[1]['where']['difficulty'] == '中等'

    def test_search_by_keyword_success(self, search_service, mock_db_manager):
        """测试关键词检索成功"""
        # 准备模拟数据
        mock_question = {
            'question_id': 'q1',
            'title': '测试题目',
            'content': '这是测试内容',
            'question_type': '单选',
            'category': '数学',
            'difficulty': '中等',
            'tags': ['标签1', '标签2'],
            'answer': '答案A',
            'explanation': '解析内容',
            'status': '已发布',
            'created_at': '2024-01-01',
            'updated_at': '2024-01-01',
            'search_score': 1.0,
            'bm25_score': 0.9
        }

        mock_db_manager.search_questions_by_keyword.return_value = [mock_question]

        # 执行检索
        result = search_service.search_by_keyword(
            query='测试',
            top_k=10,
            include_metadata=True
        )

        # 验证结果
        assert result['query'] == '测试'
        assert result['search_type'] == 'keyword'
        assert result['total_results'] == 1
        assert len(result['results']) == 1
        assert result['results'][0]['question_id'] == 'q1'
        assert result['results'][0]['search_score'] == 1.0
        assert result['results'][0]['search_type'] == 'keyword'

        # 验证调用
        mock_db_manager.search_questions_by_keyword.assert_called_once()

    def test_search_hybrid_success(self, search_service, mock_db_manager):
        """测试混合检索成功"""
        # 准备模拟数据
        semantic_result = {
            'query': '测试',
            'search_type': 'semantic',
            'results': [
                {
                    'question_id': 'q1',
                    'title': '题目1',
                    'content': '内容1',
                    'search_score': 0.95,
                    'search_type': 'semantic'
                }
            ]
        }

        keyword_result = {
            'query': '测试',
            'search_type': 'keyword',
            'results': [
                {
                    'question_id': 'q2',
                    'title': '题目2',
                    'content': '内容2',
                    'search_score': 0.85,
                    'search_type': 'keyword'
                },
                {
                    'question_id': 'q1',
                    'title': '题目1',
                    'content': '内容1',
                    'search_score': 0.90,
                    'search_type': 'keyword'
                }
            ]
        }

        # 模拟搜索服务方法
        search_service.search_by_semantic = Mock(return_value=semantic_result)
        search_service.search_by_keyword = Mock(return_value=keyword_result)

        # 执行混合检索
        result = search_service.search_hybrid(
            query='测试',
            top_k=10,
            semantic_weight=0.6,
            keyword_weight=0.4
        )

        # 验证结果
        assert result['query'] == '测试'
        assert result['search_type'] == 'hybrid'
        assert result['total_results'] == 2

        # 验证合并排序（q1的分数应该是 0.95*0.6 + 0.90*0.4 = 0.93）
        first_result = result['results'][0]
        assert first_result['question_id'] == 'q1'
        assert first_result['search_type'] == 'hybrid'

    def test_search_by_semantic_invalid_query(self, search_service):
        """测试语义检索 - 空查询"""
        with pytest.raises(ValueError, match="查询文本不能为空"):
            search_service.search_by_semantic(
                query='',
                top_k=10
            )

    def test_search_by_semantic_invalid_top_k(self, search_service, mock_db_manager):
        """测试语义检索 - 无效的top_k（会自动修正）"""
        # 准备模拟数据
        mock_db_manager.chroma_dao.search_similar.return_value = []

        # 执行检索（top_k会被自动修正为1）
        result = search_service.search_by_semantic(
            query='测试',
            top_k=-1
        )

        # 验证top_k被修正为1
        call_args = mock_db_manager.chroma_dao.search_similar.call_args
        assert call_args[1]['top_k'] == 1

    def test_search_hybrid_invalid_weights(self, search_service):
        """测试混合检索 - 无效的权重"""
        with pytest.raises(ValueError, match="必须在0-1之间"):
            search_service.search_hybrid(
                query='测试',
                semantic_weight=-0.5,
                keyword_weight=1.5
            )

    def test_search_hybrid_zero_weights(self, search_service):
        """测试混合检索 - 零权重（应使用默认权重）"""
        semantic_result = {
            'results': [],
            'query': '测试'
        }
        keyword_result = {
            'results': [],
            'query': '测试'
        }

        search_service.search_by_semantic = Mock(return_value=semantic_result)
        search_service.search_by_keyword = Mock(return_value=keyword_result)

        result = search_service.search_hybrid(
            query='测试',
            semantic_weight=0.0,
            keyword_weight=0.0
        )

        # 验证使用默认权重（0.6和0.4）
        metadata = result['metadata']
        assert abs(metadata['semantic_weight'] - 0.6) < 0.01
        assert abs(metadata['keyword_weight'] - 0.4) < 0.01

    def test_get_search_statistics(self, search_service, mock_db_manager, mock_embedding_service):
        """测试获取搜索统计信息"""
        result = search_service.get_search_statistics()

        assert 'embedding_cache_stats' in result
        assert 'database_stats' in result
        assert 'timestamp' in result

        # 验证调用
        mock_db_manager.get_statistics.assert_called_once()
        mock_embedding_service.get_cache_stats.assert_called_once()

    def test_search_with_min_similarity(self, search_service, mock_db_manager):
        """测试语义检索 - 最低相似度阈值"""
        mock_db_manager.chroma_dao.search_similar.return_value = []

        search_service.search_by_semantic(
            query='测试',
            top_k=10,
            min_similarity=0.8
        )

        # 验证传递了min_similarity参数
        call_args = mock_db_manager.chroma_dao.search_similar.call_args
        assert call_args[1]['min_similarity'] == 0.8

    def test_search_with_include_metadata_false(self, search_service, mock_db_manager):
        """测试检索 - 不包含元数据"""
        # 准备模拟数据
        mock_question = {
            'question_id': 'q1',
            'title': '测试题目',
            'content': '这是测试内容',
            'question_type': '单选',
            'category': '数学',
            'difficulty': '中等',
            'tags': ['标签1', '标签2'],
            'answer': '答案A',
            'explanation': '解析内容',
            'status': '已发布',
            'created_at': '2024-01-01',
            'updated_at': '2024-01-01'
        }

        mock_db_manager.chroma_dao.search_similar.return_value = [
            {'question_id': 'q1', 'similarity_score': 0.95}
        ]
        mock_db_manager.sqlite_dao.get_question.return_value = mock_question

        # 执行检索（不包含元数据）
        result = search_service.search_by_semantic(
            query='测试查询',
            top_k=10,
            include_metadata=False
        )

        # 验证结果不包含answer和explanation
        assert 'answer' not in result['results'][0]
        assert 'explanation' not in result['results'][0]

    def test_chroma_filter_building(self, search_service):
        """测试ChromaDB过滤条件构建"""
        filters = QuestionSearchFilter(
            category='数学',
            difficulty='中等',
            question_type='单选',
            status='已发布',
            tags=['标签1', '标签2']
        )

        where_filter = search_service._build_chroma_filter(filters)

        assert where_filter['category'] == '数学'
        assert where_filter['difficulty'] == '中等'
        assert where_filter['question_type'] == '单选'
        assert where_filter['status'] == '已发布'
        assert '$contains' in where_filter['tags']
        assert '标签1' in where_filter['tags']['$contains']
        assert '标签2' in where_filter['tags']['$contains']

    def test_search_by_keyword_with_match_mode(self, search_service, mock_db_manager):
        """测试关键词检索 - 不同的匹配模式"""
        mock_db_manager.search_questions_by_keyword.return_value = []

        # 测试AND模式
        search_service.search_by_keyword(
            query='测试 查询',
            match_mode='AND'
        )

        call_args = mock_db_manager.search_questions_by_keyword.call_args
        assert call_args[1]['match_mode'] == 'AND'

    def test_search_error_handling(self, search_service, mock_db_manager):
        """测试检索错误处理"""
        # 模拟ChromaDB检索失败
        mock_db_manager.chroma_dao.search_similar.side_effect = Exception("检索失败")

        with pytest.raises(Exception):
            search_service.search_by_semantic(
                query='测试',
                top_k=10
            )


if __name__ == '__main__':
    pytest.main([__file__, '-v'])
