"""Tests for rbsp.core.registry -- SignatureRegistry and PerfectHashTable."""

import os
import sys
import threading
import unittest

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

from rbsp.core.encoding import NaturalSignature
from rbsp.core.registry import SignatureRegistry, PerfectHashTable


class TestRegistryCRUD(RBSPTestCase):
    """Basic create/read/update/delete operations."""

    def setUp(self):
        super().setUp()
        self.reg = SignatureRegistry()

    def test_add_and_get(self):
        sig = self.sample_sigs[0]
        self.reg.add("s1", sig)
        self.assertEqual(self.reg.get("s1"), sig)

    def test_get_missing_returns_none(self):
        self.assertIsNone(self.reg.get("nonexistent"))

    def test_contains(self):
        self.reg.add("s1", self.sample_sigs[0])
        self.assertIn("s1", self.reg)
        self.assertNotIn("s2", self.reg)

    def test_len(self):
        self.assertEqual(len(self.reg), 0)
        self.reg.add("s1", self.sample_sigs[0])
        self.assertEqual(len(self.reg), 1)
        self.reg.add("s2", self.sample_sigs[1])
        self.assertEqual(len(self.reg), 2)

    def test_remove(self):
        sig = self.sample_sigs[0]
        self.reg.add("s1", sig)
        removed = self.reg.remove("s1")
        self.assertEqual(removed, sig)
        self.assertNotIn("s1", self.reg)
        self.assertEqual(len(self.reg), 0)

    def test_remove_missing(self):
        self.assertIsNone(self.reg.remove("missing"))

    def test_update_existing(self):
        self.reg.add("s1", self.sample_sigs[0])
        self.reg.add("s1", self.sample_sigs[1])
        self.assertEqual(self.reg.get("s1"), self.sample_sigs[1])
        self.assertEqual(len(self.reg), 1)

    def test_clear(self):
        for i, sig in enumerate(self.sample_sigs[:5]):
            self.reg.add(f"s{i}", sig)
        self.reg.clear()
        self.assertEqual(len(self.reg), 0)

    def test_all_ids(self):
        self.reg.add("a", self.sample_sigs[0])
        self.reg.add("b", self.sample_sigs[1])
        ids = self.reg.all_ids()
        self.assertEqual(set(ids), {"a", "b"})

    def test_items(self):
        self.reg.add("x", self.sample_sigs[0])
        items = self.reg.items()
        self.assertEqual(len(items), 1)
        self.assertEqual(items[0][0], "x")
        self.assertEqual(items[0][1], self.sample_sigs[0])

    def test_iter(self):
        self.reg.add("a", self.sample_sigs[0])
        self.reg.add("b", self.sample_sigs[1])
        sigs = list(self.reg)
        self.assertEqual(len(sigs), 2)


class TestRegistryPathIndex(RBSPTestCase):
    """Test secondary path-based lookups."""

    def setUp(self):
        super().setUp()
        self.reg = SignatureRegistry()

    def test_add_with_path(self):
        sig = self.sample_sigs[0]
        self.reg.add("s1", sig, source_path="/foo/bar.py")
        self.assertEqual(self.reg.get_by_path("/foo/bar.py"), sig)

    def test_get_by_path_missing(self):
        self.assertIsNone(self.reg.get_by_path("/nope"))

    def test_get_sig_id_by_path(self):
        self.reg.add("s1", self.sample_sigs[0], source_path="/a.py")
        self.assertEqual(self.reg.get_sig_id_by_path("/a.py"), "s1")

    def test_remove_cleans_path_index(self):
        self.reg.add("s1", self.sample_sigs[0], source_path="/a.py")
        self.reg.remove("s1")
        self.assertIsNone(self.reg.get_by_path("/a.py"))

    def test_remove_by_path(self):
        self.reg.add("s1", self.sample_sigs[0], source_path="/a.py")
        removed = self.reg.remove_by_path("/a.py")
        self.assertEqual(removed, self.sample_sigs[0])
        self.assertEqual(len(self.reg), 0)

    def test_remove_by_path_missing(self):
        self.assertIsNone(self.reg.remove_by_path("/nope"))

    def test_all_paths(self):
        self.reg.add("s1", self.sample_sigs[0], source_path="/a.py")
        self.reg.add("s2", self.sample_sigs[1], source_path="/b.py")
        paths = self.reg.all_paths()
        self.assertEqual(set(paths), {"/a.py", "/b.py"})


class TestRegistryMetadata(RBSPTestCase):
    """Test metadata attachment."""

    def setUp(self):
        super().setUp()
        self.reg = SignatureRegistry()

    def test_add_with_metadata(self):
        self.reg.add("s1", self.sample_sigs[0], metadata={"lang": "python"})
        meta = self.reg.get_metadata("s1")
        self.assertEqual(meta, {"lang": "python"})

    def test_metadata_missing(self):
        self.reg.add("s1", self.sample_sigs[0])
        self.assertIsNone(self.reg.get_metadata("s1"))

    def test_remove_clears_metadata(self):
        self.reg.add("s1", self.sample_sigs[0], metadata={"k": "v"})
        self.reg.remove("s1")
        self.assertIsNone(self.reg.get_metadata("s1"))


class TestRegistryListeners(RBSPTestCase):
    """Test event listener callbacks."""

    def setUp(self):
        super().setUp()
        self.reg = SignatureRegistry()
        self.events = []
        self.reg.add_listener(lambda ev, sid, sig: self.events.append((ev, sid)))

    def test_add_fires_event(self):
        self.reg.add("s1", self.sample_sigs[0])
        self.assertEqual(self.events, [("add", "s1")])

    def test_remove_fires_event(self):
        self.reg.add("s1", self.sample_sigs[0])
        self.events.clear()
        self.reg.remove("s1")
        self.assertEqual(self.events, [("remove", "s1")])

    def test_listener_exception_does_not_crash(self):
        def bad_listener(ev, sid, sig):
            raise RuntimeError("boom")
        self.reg.add_listener(bad_listener)
        # Should not raise
        self.reg.add("s1", self.sample_sigs[0])


class TestRegistryBulkAdd(RBSPTestCase):
    """Test bulk insertion."""

    def setUp(self):
        super().setUp()
        self.reg = SignatureRegistry()

    def test_bulk_add(self):
        entries = [(f"s{i}", sig) for i, sig in enumerate(self.sample_sigs[:5])]
        count = self.reg.bulk_add(entries)
        self.assertEqual(count, 5)
        self.assertEqual(len(self.reg), 5)
        for sid, sig in entries:
            self.assertEqual(self.reg.get(sid), sig)


class TestRegistryPersistence(RBSPTestCase):
    """Test append-only log persistence and replay."""

    def test_persist_and_replay(self):
        log_path = self.tmp_path("registry.log")
        reg1 = SignatureRegistry(backing_path=log_path)
        for i, sig in enumerate(self.sample_sigs[:5]):
            reg1.add(f"s{i}", sig)
        reg1.remove("s2")
        reg1.close()

        # Replay
        reg2 = SignatureRegistry(backing_path=log_path)
        self.assertEqual(len(reg2), 4)
        self.assertIsNotNone(reg2.get("s0"))
        self.assertIsNone(reg2.get("s2"))
        self.assertIsNotNone(reg2.get("s4"))
        reg2.close()

    def test_compact_reduces_size(self):
        log_path = self.tmp_path("registry.log")
        reg = SignatureRegistry(backing_path=log_path)
        for i in range(20):
            sig = self.generator.generate(f"doc {i}", timestamp=FIXED_TIMESTAMP)
            reg.add(f"s{i}", sig)
        for i in range(10):
            reg.remove(f"s{i}")

        size_before = os.path.getsize(log_path)
        reg.compact()
        size_after = os.path.getsize(log_path)
        self.assertLess(size_after, size_before)
        reg.close()

        # Verify compacted log replays correctly
        reg2 = SignatureRegistry(backing_path=log_path)
        self.assertEqual(len(reg2), 10)
        reg2.close()

    def test_flush(self):
        log_path = self.tmp_path("registry.log")
        reg = SignatureRegistry(backing_path=log_path)
        reg.add("s1", self.sample_sigs[0])
        reg.flush()
        self.assertGreater(os.path.getsize(log_path), 0)
        reg.close()

    def test_bulk_add_persists(self):
        log_path = self.tmp_path("registry.log")
        reg1 = SignatureRegistry(backing_path=log_path)
        entries = [(f"s{i}", sig) for i, sig in enumerate(self.sample_sigs[:5])]
        reg1.bulk_add(entries)
        reg1.close()

        reg2 = SignatureRegistry(backing_path=log_path)
        self.assertEqual(len(reg2), 5)
        reg2.close()

    def test_nonexistent_log_starts_empty(self):
        log_path = self.tmp_path("new.log")
        reg = SignatureRegistry(backing_path=log_path)
        self.assertEqual(len(reg), 0)
        reg.close()

    def test_close_idempotent(self):
        log_path = self.tmp_path("registry.log")
        reg = SignatureRegistry(backing_path=log_path)
        reg.close()
        reg.close()  # should not raise


class TestRegistryThreadSafety(RBSPTestCase):
    """Test concurrent access to the registry."""

    def test_concurrent_adds(self):
        reg = SignatureRegistry()
        errors = []
        n_threads = 8
        items_per_thread = 100

        def worker(tid):
            try:
                for i in range(items_per_thread):
                    sig = self.generator.generate(
                        f"thread_{tid}_item_{i}", timestamp=FIXED_TIMESTAMP
                    )
                    reg.add(f"t{tid}_s{i}", sig)
            except Exception as e:
                errors.append(e)

        threads = [threading.Thread(target=worker, args=(t,)) for t in range(n_threads)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        self.assertEqual(errors, [])
        self.assertEqual(len(reg), n_threads * items_per_thread)

    def test_concurrent_reads_and_writes(self):
        reg = SignatureRegistry()
        # Pre-populate
        for i in range(100):
            sig = self.generator.generate(f"pre_{i}", timestamp=FIXED_TIMESTAMP)
            reg.add(f"pre_{i}", sig)

        errors = []

        def reader():
            try:
                for _ in range(200):
                    reg.get("pre_50")
                    len(reg)
                    "pre_50" in reg
            except Exception as e:
                errors.append(e)

        def writer():
            try:
                for i in range(100):
                    sig = self.generator.generate(f"w_{i}", timestamp=FIXED_TIMESTAMP)
                    reg.add(f"w_{i}", sig)
            except Exception as e:
                errors.append(e)

        threads = [threading.Thread(target=reader) for _ in range(4)]
        threads += [threading.Thread(target=writer) for _ in range(2)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        self.assertEqual(errors, [])


class TestPerfectHashTable(RBSPTestCase):
    """Test the static perfect hash table."""

    def _build_pht(self, n: int) -> PerfectHashTable:
        entries = []
        for i in range(n):
            sig = self.generator.generate(f"pht_doc_{i}", timestamp=FIXED_TIMESTAMP)
            entries.append((f"k{i}", sig))
        return PerfectHashTable(entries), entries

    def test_empty_table(self):
        pht = PerfectHashTable([])
        self.assertEqual(len(pht), 0)
        self.assertIsNone(pht.get("anything"))
        self.assertNotIn("anything", pht)

    def test_single_entry(self):
        sig = self.sample_sigs[0]
        pht = PerfectHashTable([("only", sig)])
        self.assertEqual(len(pht), 1)
        self.assertEqual(pht.get("only"), sig)
        self.assertIsNone(pht.get("other"))

    def test_all_entries_retrievable(self):
        pht, entries = self._build_pht(50)
        self.assertEqual(len(pht), 50)
        for key, sig in entries:
            result = pht.get(key)
            self.assertIsNotNone(result, f"Key {key} not found")
            self.assertEqual(result.content_hash, sig.content_hash)

    def test_missing_key_returns_none(self):
        pht, _ = self._build_pht(20)
        self.assertIsNone(pht.get("totally_missing"))

    def test_contains(self):
        pht, entries = self._build_pht(10)
        self.assertIn(entries[0][0], pht)
        self.assertNotIn("nope", pht)

    def test_large_table(self):
        pht, entries = self._build_pht(200)
        self.assertEqual(len(pht), 200)
        for key, sig in entries:
            self.assertIsNotNone(pht.get(key))

    def test_build_from_registry(self):
        reg = SignatureRegistry()
        for i in range(30):
            sig = self.generator.generate(f"reg_doc_{i}", timestamp=FIXED_TIMESTAMP)
            reg.add(f"r{i}", sig)
        pht = reg.build_perfect_hash()
        self.assertEqual(len(pht), 30)
        for i in range(30):
            self.assertIsNotNone(pht.get(f"r{i}"))


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