"""Query Understanding Layer 测试

覆盖：归一化、短语检测、路由、同义词、幂等性、分支权重
"""

import pytest

from index1.query import (
    EnrichedQuery,
    preprocess_query,
    _normalize,
    _detect_phrases,
    _subsume_phrases,
    _check_symbol_route,
    _check_graph_route,
    _compress_long_query,
    _decompose_intent,
    _expand_synonyms,
    _build_token_query,
    _build_phrase_query,
    _build_proximity_query,
    _compute_branch_weights,
)


# ── 归一化 ──


class TestNormalize:
    def test_strip_and_nfkc(self):
        assert _normalize("  hello  ") == "hello"

    def test_fullwidth_to_halfwidth(self):
        # 全角空格 \u3000 → 半角空格
        assert _normalize("谁调用了\u3000CircuitBreaker") == "谁调用了 CircuitBreaker"

    def test_collapse_whitespace(self):
        assert _normalize("  多  余   空白  ") == "多 余 空白"

    def test_mixed_fullwidth(self):
        assert _normalize("\u3000配\u3000置\u3000") == "配 置"

    def test_empty(self):
        assert _normalize("") == ""
        assert _normalize("   ") == ""


# ── 短语检测 ──


class TestDetectPhrases:
    def test_consecutive_uppercase(self):
        phrases, sources = _detect_phrases("Event Buffer 审查修复")
        assert "Event Buffer" in phrases
        assert sources["Event Buffer"] == "consecutive_uppercase"

    def test_camelcase_split(self):
        phrases, sources = _detect_phrases("EventBuffer is here")
        assert "event buffer" in phrases
        assert sources["event buffer"] == "camelcase_split"

    def test_snake_case_split(self):
        phrases, sources = _detect_phrases("safe_db_write function")
        assert "safe db write" in phrases
        assert sources["safe db write"] == "snake_case_split"

    def test_hyphen_case_split(self):
        phrases, sources = _detect_phrases("cross-encoder model")
        assert "cross encoder" in phrases
        assert sources["cross encoder"] == "hyphen_case_split"

    def test_no_phrases(self):
        phrases, sources = _detect_phrases("配置")
        assert phrases == []
        assert sources == {}

    def test_multiple_phrases(self):
        phrases, _ = _detect_phrases("Event Buffer and Search Response")
        assert "Event Buffer" in phrases
        assert "Search Response" in phrases


class TestSubsumePhrases:
    def test_subsume_shorter(self):
        result = _subsume_phrases(["Event Buffer", "Event"])
        assert result == ["Event Buffer"]

    def test_no_subsumption(self):
        result = _subsume_phrases(["Event Buffer", "Search Response"])
        assert len(result) == 2

    def test_single(self):
        assert _subsume_phrases(["hello"]) == ["hello"]

    def test_empty(self):
        assert _subsume_phrases([]) == []


# ── 路由 ──


class TestSymbolRoute:
    def test_exact_match(self):
        symbols = {"safe_db_write", "Database", "search"}
        hits = _check_symbol_route("safe_db_write", symbols)
        assert hits == ["safe_db_write"]

    def test_in_query(self):
        symbols = {"Database", "Embedder"}
        hits = _check_symbol_route("Database query", symbols)
        assert "Database" in hits

    def test_no_match(self):
        symbols = {"safe_db_write", "Database"}
        hits = _check_symbol_route("配置 怎么加载", symbols)
        assert hits is None

    def test_with_relation_prefix(self):
        symbols = {"CircuitBreaker"}
        hits = _check_symbol_route("谁调用了 CircuitBreaker", symbols)
        assert "CircuitBreaker" in hits


class TestGraphRoute:
    def test_callers(self):
        assert _check_graph_route("谁调用了 safe_db_write") == "callers"
        assert _check_graph_route("who calls Database") == "callers"

    def test_dependencies(self):
        assert _check_graph_route("hooks.py 依赖哪些模块") == "dependencies"

    def test_references(self):
        assert _check_graph_route("CircuitBreaker 影响哪些文件") == "references"

    def test_no_relation(self):
        assert _check_graph_route("Event Buffer 审查修复") is None


# ── 长查询压缩 ──


class TestLongQueryCompress:
    def test_error_message(self):
        query = "TypeError: Cannot read property 'embed' of undefined at Embedder.process (embed.py:47) with some extra very long text that makes this query over 80 characters"
        result = _compress_long_query(query)
        assert "embed.py" in result
        assert "TypeError" in result

    def test_short_query_unchanged(self):
        query = "Event Buffer"
        assert _compress_long_query(query) == query


# ── 意图分解 ──


class TestDecomposeIntent:
    def test_mixed_query(self):
        entities, actions = _decompose_intent("Event Buffer 审查修复", ["Event Buffer"])
        assert "Event Buffer" in entities
        assert "审查" in actions
        assert "修复" in actions

    def test_pure_entity(self):
        entities, actions = _decompose_intent("safe_db_write", [])
        assert "safe_db_write" in entities
        assert actions == []


# ── 同义词 ──


class TestSynonyms:
    def test_chinese_synonym(self):
        result = _expand_synonyms("审查修复")
        assert "审查" in result
        assert "review" in result["审查"]
        assert "修复" in result
        assert "fix" in result["修复"]

    def test_no_match(self):
        result = _expand_synonyms("Event Buffer")
        assert result == {}


# ── FTS5 查询构造 ──


class TestBuildTokenQuery:
    def test_basic(self):
        result = _build_token_query("Event Buffer", {})
        assert '"Event"' in result
        assert '"Buffer"' in result
        assert "OR" in result

    def test_with_synonyms(self):
        result = _build_token_query("审查", {"审查": ["review"]})
        assert '"review"' in result


class TestBuildPhraseQuery:
    def test_basic(self):
        result = _build_phrase_query(["Event Buffer"])
        assert result is not None
        assert '"Event Buffer"' in result

    def test_none_when_empty(self):
        assert _build_phrase_query([]) is None

    def test_multiple(self):
        result = _build_phrase_query(["Event Buffer", "Search Response"])
        assert result is not None
        assert "OR" in result


# ── 分支权重 ──


class TestBranchWeights:
    def test_with_phrases(self):
        e = EnrichedQuery(original="", normalized="test", phrase_query='"Event Buffer"')
        w = _compute_branch_weights(e)
        assert "phrase" in w
        assert w["phrase"] > 0

    def test_without_phrases(self):
        e = EnrichedQuery(original="", normalized="test")
        w = _compute_branch_weights(e)
        assert w.get("phrase", 0) == 0

    def test_short_query(self):
        e = EnrichedQuery(original="", normalized="db", short_query=True)
        w = _compute_branch_weights(e)
        assert w["vector"] > w["token"]


# ── 集成测试 ──


class TestPreprocessQuery:
    @pytest.mark.parametrize(
        "query,expected_normalized,expected_route",
        [
            ("Event Buffer 审查修复", "Event Buffer 审查修复", "text_search"),
            ("  safe_db_write  ", "safe_db_write", "symbol_lookup"),
            ("谁调用了\u3000CircuitBreaker", "谁调用了 CircuitBreaker", "graph_query"),
            ("配置", "配置", "text_search"),
        ],
    )
    def test_route_and_normalize(self, query, expected_normalized, expected_route):
        symbols = {"safe_db_write", "CircuitBreaker", "Database"}
        result = preprocess_query(query, known_symbols=symbols)
        assert result.normalized == expected_normalized
        assert result.route == expected_route

    def test_phrases_detected(self):
        result = preprocess_query("Event Buffer 审查修复")
        assert "Event Buffer" in result.phrases

    def test_camelcase_phrase(self):
        result = preprocess_query("EventBuffer class")
        assert "event buffer" in result.phrases

    def test_synonyms_expanded(self):
        result = preprocess_query("审查修复")
        assert "审查" in result.synonyms

    def test_debug_trace(self):
        result = preprocess_query("Event Buffer test", debug=True)
        assert result.trace is not None
        assert "normalize" in result.trace.step_timing
        assert "phrase_detect" in result.trace.step_timing

    def test_idempotency(self):
        """preprocess(preprocess(q).normalized) 的 normalized 应与 preprocess(q).normalized 一致"""
        q = "Event Buffer 审查修复"
        first = preprocess_query(q)
        second = preprocess_query(first.normalized)
        assert first.normalized == second.normalized

    def test_empty_query(self):
        result = preprocess_query("")
        assert result.normalized == ""
        assert result.branch_weights == {"token": 0.5, "vector": 0.5}

    def test_no_symbols_provided(self):
        """known_symbols=None 时不崩溃"""
        result = preprocess_query("safe_db_write")
        assert result.route == "text_search"  # 无符号表，不触发 symbol_lookup


# ── Proximity Query (#190 P2-4) ──


class TestBuildProximityQuery:
    def test_basic_two_token_phrase(self):
        result = _build_proximity_query(["safe db write"], max_distance=5)
        assert result is not None
        assert "NEAR(" in result
        assert "5)" in result

    def test_multiple_phrases(self):
        result = _build_proximity_query(["event buffer", "search response"], max_distance=5)
        assert result is not None
        assert " OR " in result
        assert result.count("NEAR(") == 2

    def test_single_token_phrase_skipped(self):
        """单 token 短语没有 NEAR 意义"""
        result = _build_proximity_query(["hello"])
        assert result is None

    def test_empty_phrases(self):
        assert _build_proximity_query([]) is None

    def test_custom_distance(self):
        result = _build_proximity_query(["safe db write"], max_distance=10)
        assert "10)" in result

    def test_default_distance(self):
        result = _build_proximity_query(["event buffer"])
        assert "5)" in result


class TestProximityBranchWeights:
    def test_no_proximity_no_weight(self):
        """proximity_query=None 时不出现 proximity 权重"""
        eq = EnrichedQuery(original="test", normalized="test", token_query='"test"')
        eq.phrase_query = '"event buffer"'
        eq.proximity_query = None
        weights = _compute_branch_weights(eq)
        assert "proximity" not in weights

    def test_proximity_has_weight(self):
        """proximity_query 存在时出现 proximity 权重"""
        eq = EnrichedQuery(original="test", normalized="test", token_query='"test"')
        eq.phrase_query = '"event buffer"'
        eq.proximity_query = 'NEAR("event" "buffer", 5)'
        weights = _compute_branch_weights(eq)
        assert "proximity" in weights
        assert weights["proximity"] == 0.10

    def test_proximity_reduces_other_weights(self):
        """proximity 从 phrase 和 vector 各分走 0.05"""
        eq = EnrichedQuery(original="test", normalized="test", token_query='"test"')
        eq.phrase_query = '"event buffer"'
        eq.proximity_query = None
        w_no_prox = _compute_branch_weights(eq)

        eq.proximity_query = 'NEAR("event" "buffer", 5)'
        w_with_prox = _compute_branch_weights(eq)

        assert w_with_prox["phrase"] == w_no_prox["phrase"] - 0.05
        assert w_with_prox["vector"] == w_no_prox["vector"] - 0.05
        # 总权重应该相同
        assert abs(sum(w_with_prox.values()) - sum(w_no_prox.values())) < 0.001


class TestPreprocessWithProximity:
    def test_proximity_disabled_by_default(self):
        """默认 proximity_max_distance=0 时不构建 proximity_query"""
        result = preprocess_query("EventBuffer test")
        assert result.proximity_query is None

    def test_proximity_enabled(self):
        """proximity_max_distance > 0 时构建 proximity_query"""
        result = preprocess_query("EventBuffer test", proximity_max_distance=5)
        # EventBuffer 会被检测为 CamelCase 短语 "event buffer"
        if result.phrases:
            assert result.proximity_query is not None
            assert "NEAR(" in result.proximity_query
