"""Tests for rbsp.query.signal -- SignalEncoder, Signal."""

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.signal import SignalEncoder, Signal
from rbsp.core.encoding import NaturalSignature


class TestSignalEncoderDeterminism(unittest.TestCase):
    """Verify encoding is deterministic (ignoring timestamp)."""

    def setUp(self):
        self.encoder = SignalEncoder(ngram_size=3, num_hashes=128)

    def test_same_query_same_content_hash(self):
        s1 = self.encoder.encode("hello world")
        s2 = self.encoder.encode("hello world")
        self.assertEqual(s1.content_hash, s2.content_hash)

    def test_same_query_same_semantic_fingerprint(self):
        s1 = self.encoder.encode("hello world")
        s2 = self.encoder.encode("hello world")
        self.assertEqual(s1.semantic_fingerprint, s2.semantic_fingerprint)

    def test_same_query_same_structural_pattern(self):
        s1 = self.encoder.encode("hello world")
        s2 = self.encoder.encode("hello world")
        self.assertEqual(s1.structural_pattern, s2.structural_pattern)

    def test_same_query_same_minhash(self):
        s1 = self.encoder.encode("hello world")
        s2 = self.encoder.encode("hello world")
        self.assertEqual(s1.minhash_signature, s2.minhash_signature)

    def test_same_query_same_shingles(self):
        s1 = self.encoder.encode("hello world")
        s2 = self.encoder.encode("hello world")
        self.assertEqual(s1.shingles, s2.shingles)

    def test_different_queries_different_hashes(self):
        s1 = self.encoder.encode("hello world")
        s2 = self.encoder.encode("goodbye universe")
        self.assertNotEqual(s1.content_hash, s2.content_hash)


class TestSignalEncoderContentHash(unittest.TestCase):
    """Test content_hash field."""

    def setUp(self):
        self.encoder = SignalEncoder()

    def test_content_hash_is_int(self):
        sig = self.encoder.encode("test query")
        self.assertIsInstance(sig.content_hash, int)

    def test_content_hash_nonzero(self):
        sig = self.encoder.encode("test query")
        self.assertNotEqual(sig.content_hash, 0)

    def test_whitespace_stripping(self):
        s1 = self.encoder.encode("  hello  ")
        s2 = self.encoder.encode("hello")
        self.assertEqual(s1.content_hash, s2.content_hash)


class TestSignalEncoderSemanticFingerprint(unittest.TestCase):
    """Test semantic_fingerprint field."""

    def setUp(self):
        self.encoder = SignalEncoder(ngram_size=3, num_hashes=128)

    def test_semantic_fingerprint_nonzero_for_normal_text(self):
        sig = self.encoder.encode("The quick brown fox jumps over the lazy dog")
        self.assertNotEqual(sig.semantic_fingerprint, 0)

    def test_semantic_fingerprint_is_int(self):
        sig = self.encoder.encode("some text here")
        self.assertIsInstance(sig.semantic_fingerprint, int)


class TestSignalEncoderStructuralPattern(unittest.TestCase):
    """Test structural_pattern field."""

    def setUp(self):
        self.encoder = SignalEncoder()

    def test_structural_pattern_for_code(self):
        sig = self.encoder.encode("def foo():\n    return 42")
        self.assertIsInstance(sig.structural_pattern, int)

    def test_structural_pattern_deterministic(self):
        s1 = self.encoder.encode("def foo():\n    return 42")
        s2 = self.encoder.encode("def foo():\n    return 42")
        self.assertEqual(s1.structural_pattern, s2.structural_pattern)


class TestSignalEncoderQueryTokens(unittest.TestCase):
    """Test query_tokens field."""

    def setUp(self):
        self.encoder = SignalEncoder()

    def test_tokens_are_lowercased(self):
        sig = self.encoder.encode("Hello WORLD")
        self.assertEqual(sig.query_tokens, ("hello", "world"))

    def test_tokens_split_on_whitespace(self):
        sig = self.encoder.encode("foo  bar  baz")
        self.assertEqual(sig.query_tokens, ("foo", "bar", "baz"))

    def test_tokens_tuple(self):
        sig = self.encoder.encode("a b c")
        self.assertIsInstance(sig.query_tokens, tuple)


class TestSignalEncoderShingles(unittest.TestCase):
    """Test shingles field."""

    def setUp(self):
        self.encoder = SignalEncoder(ngram_size=3)

    def test_shingles_nonempty_for_normal_text(self):
        sig = self.encoder.encode("hello world")
        self.assertGreater(len(sig.shingles), 0)

    def test_shingles_frozenset(self):
        sig = self.encoder.encode("hello world")
        self.assertIsInstance(sig.shingles, frozenset)

    def test_shingles_include_char_ngrams(self):
        sig = self.encoder.encode("hello world")
        # 3-grams of "hello world" lowercased include hash("hel"), hash("ell"), etc.
        self.assertIn(hash("hel"), sig.shingles)
        self.assertIn(hash("ell"), sig.shingles)


class TestSignalMatchesMethods(unittest.TestCase):
    """Test Signal.matches_content/semantic/structural against NaturalSignature."""

    def setUp(self):
        self.encoder = SignalEncoder(ngram_size=3, num_hashes=128)

    def test_matches_content_same_text(self):
        text = "The quick brown fox jumps over the lazy dog"
        sig = self.encoder.encode(text)
        from rbsp.core.signature import SignatureGenerator
        gen = SignatureGenerator(ngram_size=3, num_hashes=128)
        nat_sig = gen.generate(text)
        self.assertTrue(sig.matches_content(nat_sig))

    def test_matches_content_different_text(self):
        sig = self.encoder.encode("hello world")
        nat_sig = NaturalSignature(content_hash=12345)
        self.assertFalse(sig.matches_content(nat_sig))

    def test_matches_semantic_zero_fingerprint(self):
        """When semantic_fingerprint is 0, matches_semantic returns False."""
        sig = Signal(
            content_hash=1,
            semantic_fingerprint=0,
            structural_pattern=0,
            query_tokens=(),
            minhash_signature=(),
            shingles=frozenset(),
            timestamp=0.0,
            raw_query="",
        )
        nat_sig = NaturalSignature(content_hash=1, semantic_hash=123)
        self.assertFalse(sig.matches_semantic(nat_sig))

    def test_matches_semantic_nonzero_match(self):
        sig = Signal(
            content_hash=1,
            semantic_fingerprint=999,
            structural_pattern=0,
            query_tokens=(),
            minhash_signature=(),
            shingles=frozenset(),
            timestamp=0.0,
            raw_query="",
        )
        nat_sig = NaturalSignature(content_hash=1, semantic_hash=999)
        self.assertTrue(sig.matches_semantic(nat_sig))

    def test_matches_semantic_nonzero_no_match(self):
        sig = Signal(
            content_hash=1,
            semantic_fingerprint=111,
            structural_pattern=0,
            query_tokens=(),
            minhash_signature=(),
            shingles=frozenset(),
            timestamp=0.0,
            raw_query="",
        )
        nat_sig = NaturalSignature(content_hash=1, semantic_hash=222)
        self.assertFalse(sig.matches_semantic(nat_sig))

    def test_matches_structural_zero(self):
        sig = Signal(
            content_hash=1,
            semantic_fingerprint=0,
            structural_pattern=0,
            query_tokens=(),
            minhash_signature=(),
            shingles=frozenset(),
            timestamp=0.0,
            raw_query="",
        )
        nat_sig = NaturalSignature(content_hash=1, structural_hash=123)
        self.assertFalse(sig.matches_structural(nat_sig))

    def test_matches_structural_match(self):
        sig = Signal(
            content_hash=1,
            semantic_fingerprint=0,
            structural_pattern=555,
            query_tokens=(),
            minhash_signature=(),
            shingles=frozenset(),
            timestamp=0.0,
            raw_query="",
        )
        nat_sig = NaturalSignature(content_hash=1, structural_hash=555)
        self.assertTrue(sig.matches_structural(nat_sig))


class TestSignalEncoderEncodeTokens(unittest.TestCase):
    """Test encode_tokens() method."""

    def setUp(self):
        self.encoder = SignalEncoder()

    def test_encode_tokens_equivalent_to_joined(self):
        tokens = ["hello", "world"]
        s1 = self.encoder.encode_tokens(tokens)
        s2 = self.encoder.encode("hello world")
        self.assertEqual(s1.content_hash, s2.content_hash)
        self.assertEqual(s1.semantic_fingerprint, s2.semantic_fingerprint)

    def test_encode_tokens_returns_signal(self):
        result = self.encoder.encode_tokens(["foo", "bar"])
        self.assertIsInstance(result, Signal)


class TestSignalEncoderEdgeCases(unittest.TestCase):
    """Test edge cases."""

    def setUp(self):
        self.encoder = SignalEncoder(ngram_size=3, num_hashes=128)

    def test_empty_query(self):
        sig = self.encoder.encode("")
        self.assertIsInstance(sig, Signal)
        self.assertEqual(sig.query_tokens, ())
        # Empty string has no shingles
        self.assertEqual(len(sig.shingles), 0)
        self.assertEqual(sig.semantic_fingerprint, 0)
        self.assertEqual(sig.minhash_signature, ())

    def test_very_short_query(self):
        sig = self.encoder.encode("ab")
        self.assertIsInstance(sig, Signal)
        # "ab" is shorter than ngram_size=3, so char n-grams are empty
        # but word shingles might still exist from word_shingles(n=2)
        # with only 1 word, word_shingles returns empty too

    def test_raw_query_preserved(self):
        sig = self.encoder.encode("  hello world  ")
        self.assertEqual(sig.raw_query, "  hello world  ")

    def test_signal_is_frozen(self):
        sig = self.encoder.encode("test")
        with self.assertRaises(AttributeError):
            sig.content_hash = 42


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