"""Tests for rbsp.query.spellcheck -- SpellChecker, _generate_deletes, _levenshtein."""

import os
import sys
import threading
import unittest

sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
from rbsp.query.spellcheck import SpellChecker, _generate_deletes, _levenshtein


# ---------------------------------------------------------------------------
# _levenshtein
# ---------------------------------------------------------------------------

class TestLevenshtein(unittest.TestCase):
    """Tests for the _levenshtein helper."""

    def test_identical_strings(self):
        self.assertEqual(_levenshtein("hello", "hello"), 0)

    def test_empty_strings(self):
        self.assertEqual(_levenshtein("", ""), 0)

    def test_one_empty(self):
        self.assertEqual(_levenshtein("abc", ""), 3)
        self.assertEqual(_levenshtein("", "xyz"), 3)

    def test_single_insertion(self):
        self.assertEqual(_levenshtein("abc", "abcd"), 1)

    def test_single_deletion(self):
        self.assertEqual(_levenshtein("abcd", "abc"), 1)

    def test_single_substitution(self):
        self.assertEqual(_levenshtein("abc", "axc"), 1)

    def test_symmetric(self):
        self.assertEqual(_levenshtein("kitten", "sitting"), _levenshtein("sitting", "kitten"))

    def test_known_distance(self):
        # "kitten" -> "sitting" is 3
        self.assertEqual(_levenshtein("kitten", "sitting"), 3)

    def test_completely_different(self):
        self.assertEqual(_levenshtein("abc", "xyz"), 3)


# ---------------------------------------------------------------------------
# _generate_deletes
# ---------------------------------------------------------------------------

class TestGenerateDeletes(unittest.TestCase):
    """Tests for the _generate_deletes helper."""

    def test_empty_word(self):
        self.assertEqual(_generate_deletes("", 2), set())

    def test_max_distance_zero(self):
        self.assertEqual(_generate_deletes("hello", 0), set())

    def test_single_deletion(self):
        deletes = _generate_deletes("abc", 1)
        # Single-character deletions of "abc": "bc", "ac", "ab"
        self.assertEqual(deletes, {"bc", "ac", "ab"})

    def test_double_deletion(self):
        deletes = _generate_deletes("abc", 2)
        # Single: "bc", "ac", "ab"
        # Double: "c", "b", "a"
        self.assertIn("bc", deletes)
        self.assertIn("a", deletes)
        self.assertIn("b", deletes)
        self.assertIn("c", deletes)

    def test_does_not_include_original(self):
        deletes = _generate_deletes("hello", 2)
        self.assertNotIn("hello", deletes)


# ---------------------------------------------------------------------------
# SpellChecker — add_word / add_words
# ---------------------------------------------------------------------------

class TestSpellCheckerAddWord(unittest.TestCase):
    """Tests for SpellChecker dictionary building."""

    def test_add_word_basic(self):
        sc = SpellChecker()
        sc.add_word("hello")
        results = sc.lookup("hello")
        self.assertEqual(len(results), 1)
        self.assertEqual(results[0][0], "hello")
        self.assertEqual(results[0][1], 0)  # exact match, distance 0

    def test_add_word_lowercases(self):
        sc = SpellChecker()
        sc.add_word("Hello")
        results = sc.lookup("hello")
        self.assertEqual(results[0][0], "hello")

    def test_add_word_frequency_accumulates(self):
        sc = SpellChecker()
        sc.add_word("hello", frequency=5)
        sc.add_word("hello", frequency=3)
        results = sc.lookup("hello")
        self.assertEqual(results[0][2], 8)  # frequency = 5 + 3

    def test_add_words_bulk(self):
        sc = SpellChecker()
        sc.add_words({"hello": 10, "world": 5, "python": 3})
        results = sc.lookup("hello")
        self.assertEqual(results[0][0], "hello")
        self.assertEqual(results[0][2], 10)


# ---------------------------------------------------------------------------
# SpellChecker — lookup
# ---------------------------------------------------------------------------

class TestSpellCheckerLookup(unittest.TestCase):
    """Tests for SpellChecker.lookup."""

    def setUp(self):
        self.sc = SpellChecker(max_edit_distance=2)
        self.sc.add_words({
            "hello": 100,
            "world": 80,
            "python": 60,
            "help": 50,
            "held": 40,
        })

    def test_exact_match(self):
        results = self.sc.lookup("hello")
        self.assertTrue(any(r[0] == "hello" and r[1] == 0 for r in results))

    def test_close_match(self):
        results = self.sc.lookup("helo")
        # "helo" should match "hello" with distance 1
        found = [r for r in results if r[0] == "hello"]
        self.assertEqual(len(found), 1)
        self.assertEqual(found[0][1], 1)

    def test_far_match_rejected(self):
        results = self.sc.lookup("xyzzy")
        # "xyzzy" is far from all dictionary words (distance > 2)
        self.assertEqual(len(results), 0)

    def test_sorted_by_distance_then_frequency(self):
        results = self.sc.lookup("hell")
        # "hello" (distance=1, freq=100) should rank before "held" (distance=1, freq=40)
        if len(results) >= 2:
            # Same distance -> higher frequency first
            same_dist = [r for r in results if r[1] == results[0][1]]
            if len(same_dist) >= 2:
                self.assertGreaterEqual(same_dist[0][2], same_dist[1][2])

    def test_top_k_limits(self):
        results = self.sc.lookup("hel", top_k=2)
        self.assertLessEqual(len(results), 2)

    def test_max_distance_parameter(self):
        results = self.sc.lookup("helo", max_distance=0)
        # "helo" is not an exact match, so no results at distance 0
        self.assertEqual(len(results), 0)

    def test_lookup_lowercases_query(self):
        results = self.sc.lookup("HELLO")
        self.assertTrue(any(r[0] == "hello" for r in results))


# ---------------------------------------------------------------------------
# SpellChecker — max_edit_distance validation
# ---------------------------------------------------------------------------

class TestSpellCheckerValidation(unittest.TestCase):
    """Tests for SpellChecker parameter validation."""

    def test_negative_max_edit_distance_raises(self):
        with self.assertRaises(ValueError):
            SpellChecker(max_edit_distance=-1)

    def test_zero_max_edit_distance_is_valid(self):
        sc = SpellChecker(max_edit_distance=0)
        sc.add_word("hello")
        # Only exact matches
        results = sc.lookup("hello")
        self.assertEqual(len(results), 1)
        results = sc.lookup("helo")
        self.assertEqual(len(results), 0)


# ---------------------------------------------------------------------------
# SpellChecker — correct_query
# ---------------------------------------------------------------------------

class TestSpellCheckerCorrectQuery(unittest.TestCase):
    """Tests for SpellChecker.correct_query."""

    def setUp(self):
        self.sc = SpellChecker(max_edit_distance=2)
        self.sc.add_words({
            "hello": 100,
            "world": 80,
            "python": 60,
        })

    def test_known_words_unchanged(self):
        corrected = self.sc.correct_query(["hello", "world"])
        self.assertEqual(corrected, ["hello", "world"])

    def test_misspelled_word_corrected(self):
        corrected = self.sc.correct_query(["helo", "world"])
        self.assertEqual(corrected[0], "hello")
        self.assertEqual(corrected[1], "world")

    def test_unknown_word_kept(self):
        corrected = self.sc.correct_query(["xyzzy"])
        self.assertEqual(corrected, ["xyzzy"])

    def test_case_insensitive(self):
        corrected = self.sc.correct_query(["HELLO"])
        self.assertEqual(corrected, ["hello"])


# ---------------------------------------------------------------------------
# SpellChecker — did_you_mean
# ---------------------------------------------------------------------------

class TestSpellCheckerDidYouMean(unittest.TestCase):
    """Tests for SpellChecker.did_you_mean."""

    def setUp(self):
        self.sc = SpellChecker(max_edit_distance=2)
        self.sc.add_words({
            "hello": 100,
            "world": 80,
            "python": 60,
        })

    def test_no_corrections_returns_none(self):
        result = self.sc.did_you_mean("hello world")
        self.assertIsNone(result)

    def test_correction_returns_string(self):
        result = self.sc.did_you_mean("helo wrld")
        self.assertIsNotNone(result)
        self.assertIn("hello", result)

    def test_empty_query_returns_none(self):
        result = self.sc.did_you_mean("")
        self.assertIsNone(result)

    def test_partial_correction(self):
        result = self.sc.did_you_mean("helo world")
        self.assertIsNotNone(result)
        self.assertIn("hello", result)
        self.assertIn("world", result)


# ---------------------------------------------------------------------------
# SpellChecker — thread safety
# ---------------------------------------------------------------------------

class TestSpellCheckerThreadSafety(unittest.TestCase):
    """Tests for SpellChecker thread safety."""

    def test_concurrent_adds_and_lookups(self):
        sc = SpellChecker(max_edit_distance=1)
        errors = []

        def add_words(prefix, count):
            try:
                for i in range(count):
                    sc.add_word(f"{prefix}{i}", frequency=1)
            except Exception as e:
                errors.append(e)

        def lookup_words():
            try:
                for _ in range(50):
                    sc.lookup("test")
            except Exception as e:
                errors.append(e)

        threads = [
            threading.Thread(target=add_words, args=("alpha", 50)),
            threading.Thread(target=add_words, args=("beta", 50)),
            threading.Thread(target=lookup_words),
            threading.Thread(target=lookup_words),
        ]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        self.assertEqual(len(errors), 0, f"Thread errors: {errors}")


if __name__ == "__main__":
    unittest.main()
