"""Tests for rbsp.query.suggest — Auto-complete / type-ahead suggestion engine."""

from __future__ import annotations

import pytest

from rbsp.query.suggest import ContextSuggester, SuggestionEntry, Suggester


# ===================================================================
# SuggestionEntry dataclass
# ===================================================================


class TestSuggestionEntry:
    def test_defaults(self) -> None:
        e = SuggestionEntry(text="hello")
        assert e.text == "hello"
        assert e.weight == 1
        assert e.payload is None

    def test_with_weight_and_payload(self) -> None:
        e = SuggestionEntry(text="hello", weight=10, payload="extra")
        assert e.weight == 10
        assert e.payload == "extra"

    def test_frozen(self) -> None:
        e = SuggestionEntry(text="hello")
        with pytest.raises(AttributeError):
            e.text = "world"  # type: ignore[misc]


# ===================================================================
# Suggester
# ===================================================================


class TestSuggesterBasic:
    def test_empty_suggester(self) -> None:
        s = Suggester()
        assert len(s) == 0
        assert s.suggest("a") == []

    def test_add_and_suggest(self) -> None:
        s = Suggester()
        s.add("hello", weight=5)
        results = s.suggest("he")
        assert len(results) == 1
        assert results[0].text == "hello"
        assert results[0].weight == 5

    def test_case_insensitive_prefix(self) -> None:
        s = Suggester()
        s.add("Hello")
        assert len(s.suggest("he")) == 1
        assert len(s.suggest("HE")) == 1
        assert len(s.suggest("He")) == 1

    def test_len_after_add(self) -> None:
        s = Suggester()
        s.add("a")
        s.add("b")
        assert len(s) == 2

    def test_suggest_no_match(self) -> None:
        s = Suggester()
        s.add("hello")
        assert s.suggest("xyz") == []

    def test_top_k_limits_results(self) -> None:
        s = Suggester()
        for i in range(20):
            s.add(f"item{i:03d}")
        results = s.suggest("item", top_k=5)
        assert len(results) == 5

    def test_ordering_by_weight_descending(self) -> None:
        s = Suggester()
        s.add("aaa", weight=1)
        s.add("aab", weight=10)
        s.add("aac", weight=5)
        results = s.suggest("a")
        weights = [r.weight for r in results]
        assert weights == sorted(weights, reverse=True)


class TestSuggesterUpdate:
    def test_update_replaces_entry(self) -> None:
        s = Suggester()
        s.add("hello", weight=1)
        s.add("hello", weight=99)
        assert len(s) == 1
        results = s.suggest("he")
        assert results[0].weight == 99

    def test_update_with_payload(self) -> None:
        s = Suggester()
        s.add("hello", payload="old")
        s.add("hello", payload="new")
        results = s.suggest("he")
        assert results[0].payload == "new"


class TestSuggesterRemove:
    def test_remove_existing(self) -> None:
        s = Suggester()
        s.add("hello")
        assert s.remove("hello") is True
        assert len(s) == 0
        assert s.suggest("he") == []

    def test_remove_nonexistent(self) -> None:
        s = Suggester()
        assert s.remove("nothing") is False

    def test_remove_cleans_prefix_map(self) -> None:
        s = Suggester()
        s.add("hello")
        s.remove("hello")
        # prefix "h" should yield nothing
        assert s.suggest("h") == []


class TestSuggesterFuzzy:
    def test_fuzzy_exact_match(self) -> None:
        s = Suggester()
        s.add("hello")
        results = s.suggest_fuzzy("hello", max_edits=1)
        assert len(results) == 1

    def test_fuzzy_one_edit(self) -> None:
        s = Suggester()
        s.add("hello")
        results = s.suggest_fuzzy("helo", max_edits=1)
        assert len(results) == 1

    def test_fuzzy_too_many_edits(self) -> None:
        s = Suggester()
        s.add("hello")
        results = s.suggest_fuzzy("xxxxx", max_edits=1)
        assert len(results) == 0

    def test_fuzzy_case_insensitive(self) -> None:
        s = Suggester()
        s.add("Hello")
        results = s.suggest_fuzzy("hello", max_edits=0)
        assert len(results) == 1

    def test_fuzzy_top_k(self) -> None:
        s = Suggester()
        for i in range(10):
            s.add(f"h{i}")
        results = s.suggest_fuzzy("h0", top_k=3, max_edits=1)
        assert len(results) <= 3


# ===================================================================
# ContextSuggester
# ===================================================================


class TestContextSuggester:
    def test_suggest_without_context(self) -> None:
        cs = ContextSuggester()
        cs.add("hello", weight=5, contexts=["python"])
        results = cs.suggest("he")
        assert len(results) == 1

    def test_suggest_with_matching_context(self) -> None:
        cs = ContextSuggester()
        cs.add("hello", contexts=["python"])
        cs.add("help", contexts=["rust"])
        results = cs.suggest("he", contexts=["python"])
        assert len(results) == 1
        assert results[0].text == "hello"

    def test_suggest_with_no_matching_context(self) -> None:
        cs = ContextSuggester()
        cs.add("hello", contexts=["python"])
        results = cs.suggest("he", contexts=["java"])
        assert len(results) == 0

    def test_suggest_multiple_contexts(self) -> None:
        cs = ContextSuggester()
        cs.add("hello", contexts=["python", "rust"])
        results = cs.suggest("he", contexts=["rust"])
        assert len(results) == 1

    def test_add_without_contexts(self) -> None:
        cs = ContextSuggester()
        cs.add("hello")
        # Without context filter, should appear
        results = cs.suggest("he")
        assert len(results) == 1
        # With context filter, should not appear (no contexts tagged)
        results = cs.suggest("he", contexts=["python"])
        assert len(results) == 0

    def test_remove_clears_contexts(self) -> None:
        cs = ContextSuggester()
        cs.add("hello", contexts=["python"])
        cs.remove("hello")
        assert len(cs) == 0
        assert cs.suggest("he") == []

    def test_update_replaces_contexts(self) -> None:
        cs = ContextSuggester()
        cs.add("hello", contexts=["python"])
        cs.add("hello", contexts=["rust"])
        results = cs.suggest("he", contexts=["python"])
        assert len(results) == 0
        results = cs.suggest("he", contexts=["rust"])
        assert len(results) == 1
