# -*- coding: utf-8 -*-
"""BM25Engine 单元测试"""
import os
import sys
import pytest

# 确保 src 在 path 中
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))

from abyss.memory.bm25 import BM25Engine, ScoredResult, tokenize


class TestTokenize:
    def test_english_tokens(self):
        tokens = tokenize("hello world foo bar")
        assert "hello" in tokens
        assert "world" in tokens
        assert "foo" in tokens
        assert "bar" in tokens

    def test_english_lowercase(self):
        """英文 token 应小写化"""
        tokens = tokenize("Hello WORLD")
        assert "hello" in tokens
        assert "world" in tokens

    def test_english_stopword_filtered(self):
        """英文停用词应被过滤"""
        tokens = tokenize("the quick brown fox")
        assert "the" not in tokens
        assert "quick" in tokens
        assert "brown" in tokens
        assert "fox" in tokens

    def test_cjk_unigram(self):
        """CJK 字符应作为 Unigram（避开停用词 '你'）"""
        tokens = tokenize("代码世界")
        assert "代" in tokens
        assert "码" in tokens
        assert "世" in tokens
        assert "界" in tokens

    def test_cjk_bigram(self):
        """CJK 相邻字符应作为 Bigram"""
        tokens = tokenize("你好世界")
        # Bigram: 你好, 好世, 世界
        assert "你好" in tokens
        assert "好世" in tokens
        assert "世界" in tokens

    def test_cjk_stopword_filtered_in_unigram(self):
        """CJK 停用词 Unigram 应被过滤"""
        tokens = tokenize("的代码")
        # "的" 是停用词，应被过滤
        assert "的" not in tokens
        assert "代" in tokens
        assert "码" in tokens
        # Bigram "的代" 仍保留（保留语序信息）
        assert "的代" in tokens

    def test_mixed_content(self):
        """中英文混合"""
        tokens = tokenize("使用 Python 写代码")
        assert "python" in tokens
        assert "代" in tokens
        assert "码" in tokens
        # 代码 bigram
        assert "代码" in tokens

    def test_empty_string(self):
        assert tokenize("") == []
        assert tokenize(None) == []

    def test_punctuation_only(self):
        """纯标点不应产生 token"""
        tokens = tokenize("...!!!???")
        # 标点不匹配英文 token 正则，也不被识别为 CJK
        assert tokens == []

    def test_snake_case(self):
        """snake_case 应作为一个 token"""
        tokens = tokenize("file_read_tool")
        assert "file_read_tool" in tokens


class TestBM25Engine:
    def test_basic_search(self):
        """基础检索：能找到包含查询词的文档"""
        engine = BM25Engine()
        engine.index([
            {"title": "Python 入门", "content": "如何使用 Python 编写 hello world"},
            {"title": "Java 入门", "content": "如何使用 Java 编写 hello world"},
            {"title": "Go 入门", "content": "如何使用 Go 编写并发程序"},
        ])
        results = engine.search("Python", top_k=3)
        assert len(results) >= 1
        # 包含 Python 的文档应排第一
        assert results[0].index == 0
        assert results[0].score > 0

    def test_title_boost(self):
        """title 中出现查询词的文档分数更高"""
        engine = BM25Engine()
        engine.index([
            {"title": "Python 入门教程", "content": "通用编程入门"},  # title 命中
            {"title": "通用教程", "content": "Python 编程入门"},      # 仅 content 命中
        ])
        results = engine.search("Python", top_k=2)
        # title 命中的应排第一
        assert results[0].index == 0
        assert results[0].score > results[1].score

    def test_empty_index(self):
        engine = BM25Engine()
        results = engine.search("foo", top_k=5)
        assert results == []

    def test_empty_query(self):
        engine = BM25Engine()
        engine.index([{"title": "t", "content": "hello world"}])
        results = engine.search("", top_k=5)
        assert results == []

    def test_no_match(self):
        """无匹配返回空"""
        engine = BM25Engine()
        engine.index([{"title": "foo", "content": "bar baz"}])
        results = engine.search("nonexistent", top_k=5)
        assert results == []

    def test_top_k_limit(self):
        """top_k 限制返回数量"""
        engine = BM25Engine()
        engine.index([
            {"title": "t1", "content": "python code"},
            {"title": "t2", "content": "python script"},
            {"title": "t3", "content": "python file"},
        ])
        results = engine.search("python", top_k=2)
        assert len(results) <= 2

    def test_cjk_search(self):
        """中文检索"""
        engine = BM25Engine()
        engine.index([
            {"title": "调试技巧", "content": "如何调试 Python 代码"},
            {"title": "测试方法", "content": "如何编写单元测试"},
        ])
        results = engine.search("调试", top_k=2)
        assert len(results) >= 1
        # 调试相关的应排第一
        assert results[0].index == 0

    def test_str_entries(self):
        """支持纯 str 条目"""
        engine = BM25Engine()
        engine.index(["hello world", "foo bar", "python code"])
        results = engine.search("python", top_k=2)
        assert len(results) == 1
        assert results[0].index == 2

    def test_scored_result_dataclass(self):
        r = ScoredResult(index=0, score=1.5)
        assert r.index == 0
        assert r.score == 1.5

    def test_doc_count(self):
        engine = BM25Engine()
        engine.index([{"title": "t", "content": "c"}])
        assert engine.doc_count == 1

    def test_repeated_query_term(self):
        """查询中重复的词应正确处理"""
        engine = BM25Engine()
        engine.index([
            {"title": "test", "content": "python python python"},
        ])
        results = engine.search("python python", top_k=1)
        assert len(results) == 1
        assert results[0].score > 0

    def test_mixed_query(self):
        """中英文混合查询"""
        engine = BM25Engine()
        engine.index([
            {"title": "Python 调试", "content": "使用 pdb 调试"},
            {"title": "Java 测试", "content": "使用 junit 测试"},
        ])
        results = engine.search("python 调试", top_k=2)
        assert len(results) >= 1
        assert results[0].index == 0
