"""Search 模块测试"""

from __future__ import annotations

from unittest.mock import AsyncMock

import pytest

from index1.config import Config
from index1.db import Database
from index1.embed import OllamaEmbedder
from index1.search import (
    SearchResponse,
    _escape_fts5_query,
    _has_cjk,
    _rrf_fusion,
    _score_to_relevance,
    search,
)


class TestHasCJK:
    def test_chinese(self):
        assert _has_cjk("清算接口") is True

    def test_japanese(self):
        assert _has_cjk("こんにちは") is True

    def test_korean(self):
        assert _has_cjk("안녕하세요") is True

    def test_english(self):
        assert _has_cjk("hello world") is False

    def test_mixed(self):
        assert _has_cjk("hello 清算") is True

    def test_empty(self):
        assert _has_cjk("") is False


class TestRRFFusion:
    def test_basic_fusion(self):
        bm25 = [(1, 10.0), (2, 8.0), (3, 5.0)]
        vec = [(2, 0.1), (3, 0.2), (4, 0.3)]

        results = _rrf_fusion(bm25, vec, k=60)

        # chunk 2 出现在两路结果中，应该排名最高
        ids = [r[0] for r in results]
        assert ids[0] == 2

    def test_no_overlap(self):
        bm25 = [(1, 10.0)]
        vec = [(2, 0.1)]

        results = _rrf_fusion(bm25, vec, k=60)
        assert len(results) == 2

    def test_weighted_fusion(self):
        """CJK 查询应该向量权重更高"""
        bm25 = [(1, 10.0)]
        vec = [(2, 0.1)]

        # 向量权重高时，vec 的结果分数更高
        results = _rrf_fusion(bm25, vec, k=60, bm25_weight=0.2, vector_weight=0.8)
        scores = {r[0]: r[1] for r in results}
        assert scores[2] > scores[1]  # vec 结果分数更高

    def test_empty_inputs(self):
        results = _rrf_fusion([], [], k=60)
        assert results == []


class TestEscapeFTS5:
    def test_simple_query(self):
        result = _escape_fts5_query("hello world")
        assert '"hello"' in result
        assert '"world"' in result
        assert "OR" in result

    def test_special_chars(self):
        result = _escape_fts5_query('hello (world) "test"')
        assert "(" not in result.replace('"hello"', "").replace('"world"', "").replace('"test"', "")

    def test_empty_query(self):
        result = _escape_fts5_query("")
        assert result == '""'


class TestScoreToRelevance:
    def test_high(self):
        assert _score_to_relevance(0.9) == "high"
        assert _score_to_relevance(0.7) == "high"

    def test_medium(self):
        assert _score_to_relevance(0.5) == "medium"
        assert _score_to_relevance(0.4) == "medium"

    def test_low(self):
        assert _score_to_relevance(0.3) == "low"
        assert _score_to_relevance(0.0) == "low"


@pytest.fixture
def config(tmp_path):
    return Config(db_path=str(tmp_path / "test.db"))


@pytest.fixture
def db(config):
    d = Database(config.db_path_resolved, config.embedding_dim)
    d.connect()
    yield d
    d.close()


@pytest.fixture
def mock_embedder():
    embedder = AsyncMock(spec=OllamaEmbedder)
    embedder.model = "nomic-embed-text"
    embedder.dim = 768
    embedder.available = False
    embedder.check_availability = AsyncMock(return_value=False)
    embedder.close = AsyncMock()
    return embedder


def _seed_db(db: Database, collection: str = "test"):
    """在数据库中插入测试数据"""
    doc_id = db.upsert_document(
        path="docs/api.md", hash="abc123",
        collection=collection, doc_type="markdown",
    )
    chunks = [
        {
            "content": "Liquidation API handles position liquidation when health factor drops below 1.2",
            "section": "## Liquidation API",
            "parent_section": "# Risk Module",
            "chunk_index": 0,
            "tags": "liquidation, risk, api",
            "embedding": None,
            "token_count": 15,
            "summary": "Liquidation API handles position liquidation.",
        },
        {
            "content": "Margin Call triggers notification to users when collateral ratio approaches threshold",
            "section": "## Margin Call",
            "parent_section": "# Risk Module",
            "chunk_index": 1,
            "tags": "margin, call, notification",
            "embedding": None,
            "token_count": 14,
            "summary": "Margin Call triggers notification.",
        },
    ]
    db.insert_chunks(doc_id, chunks)
    return doc_id


class TestSearch:
    @pytest.mark.asyncio
    async def test_bm25_only(self, config, db, mock_embedder):
        """BM25 降级搜索"""
        _seed_db(db)
        resp = await search("liquidation", config, db, mock_embedder)

        assert isinstance(resp, SearchResponse)
        assert resp.mode == "bm25_only"
        assert len(resp.results) > 0
        assert resp.results[0].source == "docs/api.md"

    @pytest.mark.asyncio
    async def test_no_results(self, config, db, mock_embedder):
        """无结果搜索"""
        _seed_db(db)
        resp = await search("nonexistent_term_xyz", config, db, mock_embedder)

        assert resp.total == 0
        assert len(resp.results) == 0

    @pytest.mark.asyncio
    async def test_collection_filter(self, config, db, mock_embedder):
        """集合过滤"""
        _seed_db(db, collection="project-a")
        resp = await search("liquidation", config, db, mock_embedder, collection="project-b")

        assert resp.total == 0

    @pytest.mark.asyncio
    async def test_result_fields(self, config, db, mock_embedder):
        """验证结果字段完整性"""
        _seed_db(db)
        resp = await search("liquidation", config, db, mock_embedder)

        r = resp.results[0]
        assert r.chunk_id > 0
        assert r.score > 0
        assert r.source is not None
        assert r.section is not None
        assert r.summary is not None
        assert r.relevance in ("high", "medium", "low")
        assert r.content is None  # non-verbose

    @pytest.mark.asyncio
    async def test_verbose_mode(self, config, db, mock_embedder):
        """verbose 模式返回完整内容"""
        _seed_db(db)
        resp = await search("liquidation", config, db, mock_embedder, verbose=True)

        r = resp.results[0]
        assert r.content is not None
        assert "Liquidation" in r.content

    @pytest.mark.asyncio
    async def test_cjk_detection(self, config, db, mock_embedder):
        """CJK 查询应该正常工作（降级到 BM25）"""
        _seed_db(db)
        resp = await search("清算接口", config, db, mock_embedder)
        # 中文查询在 BM25 only 模式下可能没有结果（FTS5 分词问题）
        assert isinstance(resp, SearchResponse)
        assert resp.mode == "bm25_only"

    @pytest.mark.asyncio
    async def test_query_ms(self, config, db, mock_embedder):
        """查询时间应大于 0"""
        _seed_db(db)
        resp = await search("liquidation", config, db, mock_embedder)
        assert resp.query_ms >= 0

    @pytest.mark.asyncio
    async def test_top_k(self, config, db, mock_embedder):
        """top_k 限制结果数"""
        _seed_db(db)
        resp = await search("risk", config, db, mock_embedder, top_k=1)
        assert len(resp.results) <= 1
