"""Tests for rbsp.core.signature -- RBSP hash and SignatureGenerator."""

import os
import sys
import unittest

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

from rbsp.core.signature import (
    SignatureGenerator,
    rbsp_hash,
    rbsp_hash_text,
    structural_hash,
    _rotl64,
)
from rbsp.core.encoding import FLAG_HAS_SEMANTIC, FLAG_HAS_STRUCTURAL


class TestRBSPHash(unittest.TestCase):
    """Test the custom RBSP hash function."""

    def test_deterministic(self):
        data = b"deterministic input"
        h1 = rbsp_hash(data)
        h2 = rbsp_hash(data)
        self.assertEqual(h1, h2)

    def test_deterministic_text(self):
        h1 = rbsp_hash_text("hello")
        h2 = rbsp_hash_text("hello")
        self.assertEqual(h1, h2)

    def test_text_matches_bytes(self):
        text = "hello world"
        self.assertEqual(rbsp_hash_text(text), rbsp_hash(text.encode("utf-8")))

    def test_different_inputs_different_hashes(self):
        h1 = rbsp_hash(b"input A")
        h2 = rbsp_hash(b"input B")
        self.assertNotEqual(h1, h2)

    def test_empty_input(self):
        h = rbsp_hash(b"")
        self.assertIsInstance(h, int)
        self.assertGreater(h, 0)

    def test_single_byte_inputs_all_different(self):
        hashes = set()
        for b in range(256):
            hashes.add(rbsp_hash(bytes([b])))
        # All 256 single-byte inputs should produce unique hashes
        self.assertEqual(len(hashes), 256)

    def test_hash_is_64_bit(self):
        h = rbsp_hash(b"test")
        self.assertLessEqual(h, 0xFFFFFFFFFFFFFFFF)
        self.assertGreaterEqual(h, 0)

    def test_seed_changes_output(self):
        data = b"same data"
        h1 = rbsp_hash(data, seed=0)
        h2 = rbsp_hash(data, seed=1)
        self.assertNotEqual(h1, h2)

    def test_collision_resistance_short_strings(self):
        """Hash 10000 short strings and check no collisions."""
        hashes = set()
        for i in range(10000):
            h = rbsp_hash(f"string_{i}".encode())
            hashes.add(h)
        self.assertEqual(len(hashes), 10000)

    def test_avalanche_basic(self):
        """Flipping one bit in input should change many bits in output."""
        data_a = b"test data for avalanche"
        data_b = bytearray(data_a)
        data_b[5] ^= 0x01  # flip one bit

        h_a = rbsp_hash(bytes(data_a))
        h_b = rbsp_hash(bytes(data_b))

        # Count differing bits
        diff = h_a ^ h_b
        bit_diff = bin(diff).count("1")
        # Expect roughly 32 bits different (50% of 64), but accept 16+
        self.assertGreater(bit_diff, 10, "Avalanche effect too weak")

    def test_cross_feed_activates(self):
        """Inputs longer than 8 bytes should activate cross-feeding."""
        short = rbsp_hash(b"1234567")  # 7 bytes, no cross-feed
        long_input = rbsp_hash(b"12345678")  # 8 bytes, triggers cross-feed
        self.assertNotEqual(short, long_input)


class TestRotl64(unittest.TestCase):
    def test_rotate_by_zero(self):
        self.assertEqual(_rotl64(1, 0), 1)

    def test_rotate_by_one(self):
        self.assertEqual(_rotl64(1, 1), 2)

    def test_rotate_wraps(self):
        val = 1 << 63
        self.assertEqual(_rotl64(val, 1), 1)


class TestStructuralHash(unittest.TestCase):
    def test_code_has_structural_hash(self):
        h = structural_hash(SAMPLE_CODE)
        self.assertNotEqual(h, 0)

    def test_prose_may_have_structural_hash(self):
        # Prose has punctuation like semicolons and parens
        h = structural_hash(SAMPLE_PROSE)
        # Just verify it runs without error
        self.assertIsInstance(h, int)

    def test_empty_text(self):
        self.assertEqual(structural_hash(""), 0)

    def test_deterministic(self):
        h1 = structural_hash(SAMPLE_CODE)
        h2 = structural_hash(SAMPLE_CODE)
        self.assertEqual(h1, h2)

    def test_different_structure_different_hash(self):
        code_a = "def foo():\n    return 1\n"
        code_b = "class Bar:\n    pass\n"
        h_a = structural_hash(code_a)
        h_b = structural_hash(code_b)
        self.assertNotEqual(h_a, h_b)


class TestSignatureGenerator(RBSPTestCase):
    """Test the high-level signature generation pipeline."""

    def test_generate_string(self):
        sig = self.generator.generate("hello world", timestamp=FIXED_TIMESTAMP)
        self.assertNotEqual(sig.content_hash, 0)
        self.assertEqual(sig.timestamp, FIXED_TIMESTAMP)

    def test_generate_bytes(self):
        sig = self.generator.generate(b"raw bytes", timestamp=FIXED_TIMESTAMP)
        self.assertNotEqual(sig.content_hash, 0)

    def test_deterministic_generation(self):
        sig1 = self.generator.generate("test", timestamp=FIXED_TIMESTAMP)
        sig2 = self.generator.generate("test", timestamp=FIXED_TIMESTAMP)
        self.assertEqual(sig1.content_hash, sig2.content_hash)
        self.assertEqual(sig1.semantic_hash, sig2.semantic_hash)
        self.assertEqual(sig1.structural_hash, sig2.structural_hash)

    def test_different_content_different_sigs(self):
        sig1 = self.generator.generate("content A", timestamp=FIXED_TIMESTAMP)
        sig2 = self.generator.generate("content B", timestamp=FIXED_TIMESTAMP)
        self.assertNotEqual(sig1.content_hash, sig2.content_hash)

    def test_semantic_flag_set_for_text(self):
        sig = self.generator.generate("enough text for shingles", timestamp=FIXED_TIMESTAMP)
        self.assertTrue(sig.flags & FLAG_HAS_SEMANTIC)

    def test_structural_flag_set_for_code(self):
        sig = self.generator.generate(SAMPLE_CODE, timestamp=FIXED_TIMESTAMP)
        self.assertTrue(sig.flags & FLAG_HAS_STRUCTURAL)

    def test_short_text_no_semantic(self):
        # Text shorter than ngram_size (3) gets no semantic hash
        sig = self.generator.generate("ab", timestamp=FIXED_TIMESTAMP)
        self.assertFalse(sig.flags & FLAG_HAS_SEMANTIC)
        self.assertEqual(sig.semantic_hash, 0)

    def test_empty_string_works(self):
        sig = self.generator.generate("", timestamp=FIXED_TIMESTAMP)
        self.assertIsNotNone(sig)
        self.assertEqual(sig.size_class, 0)

    def test_size_class_scales(self):
        small = self.generator.generate("x", timestamp=FIXED_TIMESTAMP)
        large = self.generator.generate("x" * 10000, timestamp=FIXED_TIMESTAMP)
        self.assertLess(small.size_class, large.size_class)

    def test_generate_bytes_method(self):
        raw = self.generator.generate_bytes("hello", timestamp=FIXED_TIMESTAMP)
        self.assertEqual(len(raw), 64)
        self.assertIsInstance(raw, bytes)

    def test_disable_semantic(self):
        gen = SignatureGenerator(enable_semantic=False)
        sig = gen.generate("some text for testing", timestamp=FIXED_TIMESTAMP)
        self.assertEqual(sig.semantic_hash, 0)
        self.assertFalse(sig.flags & FLAG_HAS_SEMANTIC)

    def test_disable_structural(self):
        gen = SignatureGenerator(enable_structural=False)
        sig = gen.generate(SAMPLE_CODE, timestamp=FIXED_TIMESTAMP)
        self.assertEqual(sig.structural_hash, 0)
        self.assertFalse(sig.flags & FLAG_HAS_STRUCTURAL)

    def test_generate_from_file(self):
        path = self.make_sample_file("test.txt", SAMPLE_PROSE)
        with open(path, "r", encoding="utf-8") as f:
            content = f.read()
        sig = self.generator.generate(content, timestamp=FIXED_TIMESTAMP)
        self.assertNotEqual(sig.content_hash, 0)
        self.assertTrue(sig.flags & FLAG_HAS_SEMANTIC)

    def test_similar_content_similar_semantic_hash(self):
        """Two very similar texts should produce signatures where the
        semantic hashes are more alike than dissimilar texts."""
        # This is a probabilistic check -- we just verify similar texts
        # share more shingle overlap than dissimilar ones
        sig_a = self.generator.generate(SAMPLE_TEXTS[0], timestamp=FIXED_TIMESTAMP)
        sig_b = self.generator.generate(SAMPLE_TEXTS[1], timestamp=FIXED_TIMESTAMP)
        sig_c = self.generator.generate(SAMPLE_TEXTS[2], timestamp=FIXED_TIMESTAMP)
        # content hashes must differ
        self.assertNotEqual(sig_a.content_hash, sig_b.content_hash)
        self.assertNotEqual(sig_a.content_hash, sig_c.content_hash)
        # All three should have semantic hashes
        self.assertNotEqual(sig_a.semantic_hash, 0)
        self.assertNotEqual(sig_b.semantic_hash, 0)


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