"""Tests for rbsp.core.fst -- Finite State Transducer."""

import os
import sys
import unittest

sys.path.insert(0, os.path.dirname(__file__))
from conftest import RBSPTestCase

from rbsp.core.fst import FST, FSTBuilder


# ---------------------------------------------------------------------------
# Builder tests
# ---------------------------------------------------------------------------


class TestFSTBuilderBasic(RBSPTestCase):
    """FSTBuilder construction and validation."""

    def test_build_empty(self):
        builder = FSTBuilder()
        fst = builder.finish()
        self.assertEqual(len(fst), 0)

    def test_build_single_key(self):
        builder = FSTBuilder()
        builder.add("hello", 42)
        fst = builder.finish()
        self.assertEqual(len(fst), 1)
        self.assertEqual(fst.get("hello"), 42)

    def test_out_of_order_raises(self):
        builder = FSTBuilder()
        builder.add("banana", 1)
        with self.assertRaises(ValueError):
            builder.add("apple", 2)

    def test_duplicate_key_raises(self):
        builder = FSTBuilder()
        builder.add("alpha", 0)
        with self.assertRaises(ValueError):
            builder.add("alpha", 1)

    def test_add_after_finish_raises(self):
        builder = FSTBuilder()
        builder.add("a", 0)
        builder.finish()
        with self.assertRaises(ValueError):
            builder.add("b", 1)

    def test_double_finish_raises(self):
        builder = FSTBuilder()
        builder.finish()
        with self.assertRaises(ValueError):
            builder.finish()


# ---------------------------------------------------------------------------
# Exact lookup
# ---------------------------------------------------------------------------


class TestFSTExactLookup(RBSPTestCase):
    """FST.get() and FST.has_key() exact match."""

    @classmethod
    def setUpClass(cls):
        builder = FSTBuilder()
        words = ["ant", "ante", "anti", "apple", "apply", "bee", "been", "beer"]
        for i, w in enumerate(words):
            builder.add(w, i)
        cls.fst = builder.finish()
        cls.words = words

    def test_all_keys_found(self):
        for i, w in enumerate(self.words):
            self.assertEqual(self.fst.get(w), i)

    def test_has_key_true(self):
        for w in self.words:
            self.assertTrue(self.fst.has_key(w))

    def test_missing_key_returns_none(self):
        self.assertIsNone(self.fst.get("antler"))
        self.assertIsNone(self.fst.get("b"))
        self.assertIsNone(self.fst.get(""))

    def test_has_key_false(self):
        self.assertFalse(self.fst.has_key("zoo"))

    def test_prefix_not_a_key(self):
        # "an" is a prefix of "ant" but not a stored key
        self.assertIsNone(self.fst.get("an"))
        self.assertFalse(self.fst.has_key("an"))

    def test_len(self):
        self.assertEqual(len(self.fst), 8)

    def test_contains_dunder(self):
        self.assertIn("apple", self.fst)
        self.assertNotIn("zebra", self.fst)

    def test_contains_non_string(self):
        self.assertNotIn(42, self.fst)


# ---------------------------------------------------------------------------
# Prefix search
# ---------------------------------------------------------------------------


class TestFSTPrefixSearch(RBSPTestCase):
    """FST.prefix_search() tests."""

    @classmethod
    def setUpClass(cls):
        builder = FSTBuilder()
        words = ["ant", "ante", "anti", "apple", "apply", "bee", "been", "beer"]
        for i, w in enumerate(words):
            builder.add(w, i)
        cls.fst = builder.finish()

    def test_prefix_ant(self):
        results = self.fst.prefix_search("ant")
        keys = [k for k, v in results]
        self.assertEqual(keys, ["ant", "ante", "anti"])

    def test_prefix_app(self):
        results = self.fst.prefix_search("app")
        keys = [k for k, v in results]
        self.assertEqual(keys, ["apple", "apply"])

    def test_prefix_bee(self):
        results = self.fst.prefix_search("bee")
        keys = [k for k, v in results]
        self.assertEqual(keys, ["bee", "been", "beer"])

    def test_prefix_entire_key(self):
        results = self.fst.prefix_search("apple")
        self.assertEqual(results, [("apple", 3)])

    def test_prefix_no_match(self):
        results = self.fst.prefix_search("z")
        self.assertEqual(results, [])

    def test_empty_prefix_returns_all(self):
        results = self.fst.prefix_search("")
        self.assertEqual(len(results), 8)

    def test_prefix_results_sorted(self):
        results = self.fst.prefix_search("")
        keys = [k for k, v in results]
        self.assertEqual(keys, sorted(keys))


# ---------------------------------------------------------------------------
# Fuzzy search
# ---------------------------------------------------------------------------


class TestFSTFuzzySearch(RBSPTestCase):
    """FST.fuzzy_search() Levenshtein distance tests."""

    @classmethod
    def setUpClass(cls):
        builder = FSTBuilder()
        words = ["ant", "ante", "anti", "apple", "apply", "bee", "been", "beer"]
        for i, w in enumerate(words):
            builder.add(w, i)
        cls.fst = builder.finish()

    def test_exact_match_distance_zero(self):
        results = self.fst.fuzzy_search("ant", max_edit_distance=1)
        # Should include "ant" at distance 0
        exact = [(k, v, d) for k, v, d in results if d == 0]
        self.assertEqual(len(exact), 1)
        self.assertEqual(exact[0][0], "ant")

    def test_one_edit_insertion(self):
        # "ant" -> "ante" is one insertion (distance 1)
        results = self.fst.fuzzy_search("ant", max_edit_distance=1)
        keys = {k for k, v, d in results}
        self.assertIn("ante", keys)
        self.assertIn("anti", keys)

    def test_one_edit_deletion(self):
        # "ante" -> "ant" is one deletion
        results = self.fst.fuzzy_search("ante", max_edit_distance=1)
        keys = {k for k, v, d in results}
        self.assertIn("ant", keys)

    def test_one_edit_substitution(self):
        # "beer" -> "been" is one substitution (r->n)
        results = self.fst.fuzzy_search("beer", max_edit_distance=1)
        keys = {k for k, v, d in results}
        self.assertIn("been", keys)
        self.assertIn("bee", keys)  # deletion

    def test_no_match_at_distance_zero(self):
        results = self.fst.fuzzy_search("xyz", max_edit_distance=0)
        self.assertEqual(results, [])

    def test_distance_two(self):
        # "appli" -> "apple" is 1 sub, "apply" is 1 sub
        results = self.fst.fuzzy_search("appli", max_edit_distance=2)
        keys = {k for k, v, d in results}
        self.assertIn("apple", keys)
        self.assertIn("apply", keys)

    def test_results_sorted_by_distance_then_key(self):
        results = self.fst.fuzzy_search("bee", max_edit_distance=2)
        # Verify sorting: first by distance, then by key
        for i in range(len(results) - 1):
            d1, k1 = results[i][2], results[i][0]
            d2, k2 = results[i + 1][2], results[i + 1][0]
            self.assertTrue(
                (d1, k1) <= (d2, k2),
                f"Results not sorted: ({k1}, {d1}) before ({k2}, {d2})"
            )


# ---------------------------------------------------------------------------
# Suffix deduplication
# ---------------------------------------------------------------------------


class TestFSTSuffixDedup(RBSPTestCase):
    """Verify suffix deduplication merges identical sub-automata."""

    def test_shared_suffix_nodes(self):
        # Keys ending in the same suffix with the same final value can share
        # sub-automata.  "fasting" and "lasting" both map to value 0 so the
        # entire "asting" chain (including the final "g") is identical.
        builder = FSTBuilder()
        builder.add("fasting", 0)
        builder.add("lasting", 0)
        fst = builder.finish()

        self.assertEqual(fst.get("fasting"), 0)
        self.assertEqual(fst.get("lasting"), 0)
        self.assertEqual(len(fst), 2)

        # Count unique node objects in the trie.
        def count_nodes(node, visited):
            nid = id(node)
            if nid in visited:
                return 0
            visited.add(nid)
            total = 1
            for child in node.children.values():
                total += count_nodes(child, visited)
            return total

        node_count = count_nodes(fst._root, set())
        # Without dedup: root + f + l + a-s-t-i-n-g + a-s-t-i-n-g = 15 nodes
        # With dedup: root + f + l + shared(a-s-t-i-n-g) = 9 unique nodes
        self.assertLess(node_count, 15)

    def test_no_dedup_when_values_differ_midstream(self):
        # Even with shared structure, different final values keep nodes separate
        builder = FSTBuilder()
        builder.add("ab", 10)
        builder.add("cb", 20)
        fst = builder.finish()
        self.assertEqual(fst.get("ab"), 10)
        self.assertEqual(fst.get("cb"), 20)


# ---------------------------------------------------------------------------
# Edge cases
# ---------------------------------------------------------------------------


class TestFSTEdgeCases(RBSPTestCase):
    """Edge cases and boundary conditions."""

    def test_single_char_keys(self):
        builder = FSTBuilder()
        for i, ch in enumerate("abcxyz"):
            builder.add(ch, i)
        fst = builder.finish()
        self.assertEqual(fst.get("a"), 0)
        self.assertEqual(fst.get("z"), 5)
        self.assertEqual(len(fst), 6)

    def test_long_key(self):
        builder = FSTBuilder()
        long_key = "a" * 1000
        builder.add(long_key, 99)
        fst = builder.finish()
        self.assertEqual(fst.get(long_key), 99)

    def test_many_keys(self):
        builder = FSTBuilder()
        words = sorted([f"word{i:06d}" for i in range(10_000)])
        for i, w in enumerate(words):
            builder.add(w, i)
        fst = builder.finish()
        self.assertEqual(len(fst), 10_000)
        self.assertEqual(fst.get("word000000"), 0)
        self.assertEqual(fst.get("word009999"), 9999)
        self.assertIsNone(fst.get("word010000"))

    def test_value_zero(self):
        builder = FSTBuilder()
        builder.add("zero", 0)
        fst = builder.finish()
        self.assertEqual(fst.get("zero"), 0)
        self.assertTrue(fst.has_key("zero"))

    def test_prefix_search_on_empty_fst(self):
        fst = FSTBuilder().finish()
        self.assertEqual(fst.prefix_search("anything"), [])

    def test_fuzzy_search_on_empty_fst(self):
        fst = FSTBuilder().finish()
        self.assertEqual(fst.fuzzy_search("anything"), [])


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