"""Tests for rbsp.query.resonance -- ResonanceMatcher, SignatureRegistry, BloomFilter, ResonanceHit."""

import os
import sys
import unittest

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

from rbsp.query.resonance import (
    BloomFilter,
    ResonanceHit,
    ResonanceMatcher,
    SignatureRegistry,
)
from rbsp.query.signal import Signal, SignalEncoder
from rbsp.core.encoding import NaturalSignature
from rbsp.core.signature import SignatureGenerator
from rbsp.core.lsh import LSHIndex, ngram_shingles, word_shingles


# ---------------------------------------------------------------------------
# BloomFilter (the one in resonance.py - counting bloom with int keys)
# ---------------------------------------------------------------------------

class TestResonanceBloomFilterAdd(unittest.TestCase):
    """Test BloomFilter.add and might_contain."""

    def setUp(self):
        self.bf = BloomFilter(size=1024, num_hashes=5)

    def test_add_and_might_contain(self):
        self.bf.add(42)
        self.assertTrue(self.bf.might_contain(42))

    def test_not_added_probably_not_contained(self):
        # Fresh filter should not contain arbitrary keys
        self.assertFalse(self.bf.might_contain(99999))

    def test_multiple_keys(self):
        for k in [1, 2, 3, 100, 200]:
            self.bf.add(k)
        for k in [1, 2, 3, 100, 200]:
            self.assertTrue(self.bf.might_contain(k))


class TestResonanceBloomFilterRemove(unittest.TestCase):
    """Test BloomFilter.remove (counting)."""

    def setUp(self):
        self.bf = BloomFilter(size=4096, num_hashes=5)

    def test_remove_after_add(self):
        self.bf.add(42)
        self.assertTrue(self.bf.might_contain(42))
        self.bf.remove(42)
        self.assertFalse(self.bf.might_contain(42))

    def test_remove_without_add_no_error(self):
        # Removing key that was never added should not crash
        self.bf.remove(999)

    def test_add_twice_remove_once_still_present(self):
        self.bf.add(10)
        self.bf.add(10)
        self.bf.remove(10)
        # Since counters were incremented twice, one remove leaves count > 0
        self.assertTrue(self.bf.might_contain(10))


# ---------------------------------------------------------------------------
# SignatureRegistry
# ---------------------------------------------------------------------------

class TestSignatureRegistryRegister(RBSPTestCase):
    """Test register and basic lookup."""

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

    def test_register_and_len(self):
        self.registry.register("item1", self.sample_sigs[0])
        self.assertEqual(len(self.registry), 1)

    def test_register_multiple(self):
        for i, sig in enumerate(self.sample_sigs):
            self.registry.register(f"item_{i}", sig)
        self.assertEqual(len(self.registry), len(self.sample_sigs))

    def test_contains(self):
        self.registry.register("x", self.sample_sigs[0])
        self.assertIn("x", self.registry)
        self.assertNotIn("y", self.registry)

    def test_get_item(self):
        meta = {"path": "foo.py"}
        self.registry.register("x", self.sample_sigs[0], metadata=meta)
        result = self.registry.get_item("x")
        self.assertIsNotNone(result)
        sig, m = result
        self.assertEqual(sig.content_hash, self.sample_sigs[0].content_hash)
        self.assertEqual(m["path"], "foo.py")

    def test_get_item_not_found(self):
        self.assertIsNone(self.registry.get_item("nonexistent"))


class TestSignatureRegistryUnregister(RBSPTestCase):
    """Test unregister."""

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

    def test_unregister_existing(self):
        self.registry.register("a", self.sample_sigs[0])
        result = self.registry.unregister("a")
        self.assertTrue(result)
        self.assertNotIn("a", self.registry)
        self.assertEqual(len(self.registry), 0)

    def test_unregister_nonexistent(self):
        result = self.registry.unregister("does_not_exist")
        self.assertFalse(result)


class TestSignatureRegistryLookupContent(RBSPTestCase):
    """Test lookup_content."""

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

    def test_lookup_content_found(self):
        sig = self.sample_sigs[0]
        self.registry.register("item1", sig)
        results = self.registry.lookup_content(sig.content_hash)
        self.assertEqual(len(results), 1)
        self.assertEqual(results[0][0], "item1")

    def test_lookup_content_not_found(self):
        results = self.registry.lookup_content(0xDEADBEEF)
        self.assertEqual(results, [])


class TestSignatureRegistryLookupSemantic(RBSPTestCase):
    """Test lookup_semantic."""

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

    def test_lookup_semantic_found(self):
        sig = self.sample_sigs[0]
        if sig.semantic_hash:
            self.registry.register("s1", sig)
            ids = self.registry.lookup_semantic(sig.semantic_hash)
            self.assertIn("s1", ids)

    def test_lookup_semantic_not_found(self):
        ids = self.registry.lookup_semantic(0xBADBAD)
        self.assertEqual(ids, set())


class TestSignatureRegistryLookupStructural(RBSPTestCase):
    """Test lookup_structural."""

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

    def test_lookup_structural_found(self):
        # Use a code sample that has structural hash
        gen = SignatureGenerator(ngram_size=3, num_hashes=128)
        sig = gen.generate(SAMPLE_CODE, timestamp=FIXED_TIMESTAMP)
        if sig.structural_hash:
            self.registry.register("code1", sig)
            ids = self.registry.lookup_structural(sig.structural_hash)
            self.assertIn("code1", ids)

    def test_lookup_structural_not_found(self):
        ids = self.registry.lookup_structural(0xFACEFACE)
        self.assertEqual(ids, set())


# ---------------------------------------------------------------------------
# ResonanceMatcher
# ---------------------------------------------------------------------------

class TestResonanceMatcherExact(RBSPTestCase):
    """Test exact content hash matching."""

    def setUp(self):
        super().setUp()
        self.registry = SignatureRegistry()
        self.encoder = SignalEncoder(ngram_size=3, num_hashes=128)
        # Register some sample signatures
        for i, sig in enumerate(self.sample_sigs):
            self.registry.register(f"item_{i}", sig, metadata={"index": i})

    def test_exact_match_found(self):
        matcher = ResonanceMatcher(self.registry)
        # Encode the same text as sample_sigs[0]
        signal = self.encoder.encode(SAMPLE_TEXTS[0])
        hits = matcher.match(signal)
        item_ids = [h.item_id for h in hits]
        self.assertIn("item_0", item_ids)

    def test_exact_match_score(self):
        matcher = ResonanceMatcher(self.registry)
        signal = self.encoder.encode(SAMPLE_TEXTS[0])
        hits = matcher.match(signal)
        exact_hit = [h for h in hits if h.item_id == "item_0"]
        self.assertEqual(len(exact_hit), 1)
        self.assertEqual(exact_hit[0].exact_score, 1.0)

    def test_no_match_for_unknown_text(self):
        matcher = ResonanceMatcher(self.registry)
        signal = self.encoder.encode("this text is definitely not in the sample set xyz123")
        hits = matcher.match(signal)
        # Might get semantic/structural hits but likely no exact
        exact_hits = [h for h in hits if h.exact_score > 0]
        self.assertEqual(len(exact_hits), 0)


class TestResonanceMatcherSemantic(RBSPTestCase):
    """Test semantic matching via hash + LSH."""

    def setUp(self):
        super().setUp()
        self.registry = SignatureRegistry()
        self.lsh = LSHIndex(num_hashes=128, bands=16, rows=8)
        self.encoder = SignalEncoder(ngram_size=3, num_hashes=128)

        for i, text in enumerate(SAMPLE_TEXTS):
            sig = self.sample_sigs[i]
            self.registry.register(f"item_{i}", sig)
            shingles = ngram_shingles(text, n=3) | word_shingles(text, n=2)
            if shingles:
                self.lsh.insert(f"item_{i}", shingles)

    def test_semantic_match_with_lsh(self):
        matcher = ResonanceMatcher(self.registry, lsh_index=self.lsh)
        # Query the exact same text as SAMPLE_TEXTS[0] to guarantee LSH match
        signal = self.encoder.encode(SAMPLE_TEXTS[0])
        hits = matcher.match(signal, top_k=10)
        # Should find at least the exact match plus potential LSH matches
        self.assertGreater(len(hits), 0)

    def test_top_k_limits_results(self):
        matcher = ResonanceMatcher(self.registry, lsh_index=self.lsh)
        signal = self.encoder.encode("programming language")
        hits = matcher.match(signal, top_k=2)
        self.assertLessEqual(len(hits), 2)


class TestResonanceMatcherStructural(RBSPTestCase):
    """Test structural hash matching."""

    def setUp(self):
        super().setUp()
        self.registry = SignatureRegistry()
        self.encoder = SignalEncoder()
        # Register code sample with structural hash
        gen = SignatureGenerator(ngram_size=3, num_hashes=128)
        self.code_sig = gen.generate(SAMPLE_CODE, timestamp=FIXED_TIMESTAMP)
        self.registry.register("code_item", self.code_sig)

    def test_structural_match(self):
        matcher = ResonanceMatcher(self.registry)
        # If we encode the same code, structural pattern should match
        signal = self.encoder.encode(SAMPLE_CODE)
        hits = matcher.match(signal)
        struct_hits = [h for h in hits if h.structural_score > 0]
        # Same code text should produce same structural hash
        if self.code_sig.structural_hash != 0 and signal.structural_pattern != 0:
            if signal.structural_pattern == self.code_sig.structural_hash:
                self.assertGreater(len(struct_hits), 0)


class TestResonanceMatcherScoring(unittest.TestCase):
    """Test resonance score computation and weights."""

    def test_custom_weights(self):
        registry = SignatureRegistry()
        sig = NaturalSignature(content_hash=100, semantic_hash=200, structural_hash=300, timestamp=FIXED_TIMESTAMP)
        registry.register("item1", sig)

        matcher = ResonanceMatcher(
            registry,
            exact_weight=0.5,
            semantic_weight=0.3,
            structural_weight=0.2,
        )
        # Create a signal matching the content hash
        signal = Signal(
            content_hash=100,
            semantic_fingerprint=200,
            structural_pattern=300,
            query_tokens=(),
            minhash_signature=(),
            shingles=frozenset(),
            timestamp=0.0,
            raw_query="",
        )
        hits = matcher.match(signal)
        self.assertEqual(len(hits), 1)
        hit = hits[0]
        expected = 0.5 * 1.0 + 0.3 * 1.0 + 0.2 * 1.0
        self.assertAlmostEqual(hit.resonance_score, expected)

    def test_min_score_filter(self):
        registry = SignatureRegistry()
        sig = NaturalSignature(content_hash=100, timestamp=FIXED_TIMESTAMP)
        registry.register("item1", sig)

        matcher = ResonanceMatcher(registry)
        signal = Signal(
            content_hash=100,
            semantic_fingerprint=0,
            structural_pattern=0,
            query_tokens=(),
            minhash_signature=(),
            shingles=frozenset(),
            timestamp=0.0,
            raw_query="",
        )
        # With a very high min_score, the result should be filtered
        hits = matcher.match(signal, min_score=0.99)
        self.assertEqual(len(hits), 0)


class TestResonanceHit(unittest.TestCase):
    """Test ResonanceHit dataclass."""

    def test_fields(self):
        sig = NaturalSignature(content_hash=1)
        hit = ResonanceHit(
            item_id="test",
            signature=sig,
            resonance_score=0.8,
            exact_score=0.5,
            semantic_score=0.2,
            structural_score=0.1,
            metadata={"key": "value"},
        )
        self.assertEqual(hit.item_id, "test")
        self.assertAlmostEqual(hit.resonance_score, 0.8)
        self.assertEqual(hit.metadata, {"key": "value"})

    def test_default_scores(self):
        sig = NaturalSignature(content_hash=1)
        hit = ResonanceHit(item_id="x", signature=sig, resonance_score=0.5)
        self.assertEqual(hit.exact_score, 0.0)
        self.assertEqual(hit.semantic_score, 0.0)
        self.assertEqual(hit.structural_score, 0.0)
        self.assertIsNone(hit.metadata)

    def test_frozen(self):
        sig = NaturalSignature(content_hash=1)
        hit = ResonanceHit(item_id="x", signature=sig, resonance_score=0.5)
        with self.assertRaises(AttributeError):
            hit.resonance_score = 1.0


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