"""Tests for rbsp.query.semantic -- SemanticResolver, SemanticCluster, SemanticResult."""

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.semantic import SemanticResolver, SemanticCluster, SemanticResult
from rbsp.query.signal import SignalEncoder, Signal
from rbsp.core.lsh import LSHIndex, ngram_shingles, word_shingles


def _build_lsh_with_samples(texts, num_hashes=128, bands=16, rows=8, ngram_size=3):
    """Insert sample texts into an LSH index and return (lsh_index, ids)."""
    lsh = LSHIndex(num_hashes=num_hashes, bands=bands, rows=rows)
    ids = []
    for i, text in enumerate(texts):
        item_id = f"doc_{i}"
        shingles = ngram_shingles(text, n=ngram_size) | word_shingles(text, n=2)
        if shingles:
            lsh.insert(item_id, shingles)
            ids.append(item_id)
    return lsh, ids


# ---------------------------------------------------------------------------
# SemanticCluster
# ---------------------------------------------------------------------------

class TestSemanticClusterBasic(unittest.TestCase):
    """Test SemanticCluster add, remove, len, contains."""

    def test_add_and_len(self):
        c = SemanticCluster(cluster_id=(0, 42))
        c.add("a")
        c.add("b")
        self.assertEqual(len(c), 2)

    def test_contains(self):
        c = SemanticCluster(cluster_id=(0, 1))
        c.add("x")
        self.assertIn("x", c)
        self.assertNotIn("y", c)

    def test_remove(self):
        c = SemanticCluster(cluster_id=(0, 1))
        c.add("a")
        c.add("b")
        c.remove("a")
        self.assertNotIn("a", c)
        self.assertEqual(len(c), 1)

    def test_remove_nonexistent_no_error(self):
        c = SemanticCluster(cluster_id=(0, 1))
        c.remove("nope")  # should not raise

    def test_empty_cluster(self):
        c = SemanticCluster(cluster_id=(0, 0))
        self.assertEqual(len(c), 0)

    def test_cluster_id(self):
        c = SemanticCluster(cluster_id=(5, 100))
        self.assertEqual(c.cluster_id, (5, 100))


# ---------------------------------------------------------------------------
# SemanticResolver.resolve()
# ---------------------------------------------------------------------------

class TestSemanticResolverResolve(unittest.TestCase):
    """Test resolve() with LSH."""

    def setUp(self):
        # Use texts that have enough content for meaningful shingles
        self.texts = [t for t in SAMPLE_TEXTS if len(t) >= 10]
        self.lsh, self.ids = _build_lsh_with_samples(self.texts)
        self.resolver = SemanticResolver(self.lsh, min_similarity=0.0)
        self.encoder = SignalEncoder(ngram_size=3, num_hashes=128)

    def test_resolve_returns_results(self):
        if not self.ids:
            self.skipTest("No items inserted into LSH")
        signal = self.encoder.encode(self.texts[0])
        results = self.resolver.resolve(signal, top_k=10)
        self.assertIsInstance(results, list)
        # Should find at least the exact match
        self.assertGreater(len(results), 0)

    def test_resolve_results_are_semantic_result(self):
        if not self.ids:
            self.skipTest("No items inserted into LSH")
        signal = self.encoder.encode(self.texts[0])
        results = self.resolver.resolve(signal, top_k=5)
        for r in results:
            self.assertIsInstance(r, SemanticResult)

    def test_resolve_top_k(self):
        if not self.ids:
            self.skipTest("No items inserted into LSH")
        signal = self.encoder.encode(self.texts[0])
        results = self.resolver.resolve(signal, top_k=2)
        self.assertLessEqual(len(results), 2)

    def test_resolve_min_similarity_filter(self):
        if not self.ids:
            self.skipTest("No items inserted into LSH")
        signal = self.encoder.encode(self.texts[0])
        results = self.resolver.resolve(signal, min_similarity=0.99)
        # Very high threshold should filter most results
        for r in results:
            self.assertGreaterEqual(r.similarity, 0.99)

    def test_resolve_empty_shingles_returns_empty(self):
        # Signal with no shingles
        signal = Signal(
            content_hash=1,
            semantic_fingerprint=0,
            structural_pattern=0,
            query_tokens=(),
            minhash_signature=(),
            shingles=frozenset(),
            timestamp=0.0,
            raw_query="",
        )
        results = self.resolver.resolve(signal)
        self.assertEqual(results, [])


# ---------------------------------------------------------------------------
# SemanticResolver.resolve_text()
# ---------------------------------------------------------------------------

class TestSemanticResolverResolveText(unittest.TestCase):
    """Test resolve_text() convenience method."""

    def setUp(self):
        self.texts = [t for t in SAMPLE_TEXTS if len(t) >= 10]
        self.lsh, self.ids = _build_lsh_with_samples(self.texts)
        self.resolver = SemanticResolver(self.lsh, min_similarity=0.0)

    def test_resolve_text_returns_results(self):
        if not self.ids:
            self.skipTest("No items inserted into LSH")
        results = self.resolver.resolve_text(self.texts[0])
        self.assertGreater(len(results), 0)

    def test_resolve_text_empty_string(self):
        results = self.resolver.resolve_text("")
        self.assertEqual(results, [])

    def test_resolve_text_short_string(self):
        results = self.resolver.resolve_text("ab")
        # "ab" has no 3-grams or 2-word shingles
        self.assertEqual(results, [])


# ---------------------------------------------------------------------------
# SemanticResolver.expand_neighbors()
# ---------------------------------------------------------------------------

class TestSemanticResolverExpandNeighbors(unittest.TestCase):
    """Test expand_neighbors()."""

    def setUp(self):
        self.texts = [t for t in SAMPLE_TEXTS if len(t) >= 10]
        self.lsh, self.ids = _build_lsh_with_samples(self.texts)
        self.resolver = SemanticResolver(self.lsh, min_similarity=0.0)

    def test_expand_neighbors_existing_item(self):
        if len(self.ids) < 2:
            self.skipTest("Need at least 2 items")
        results = self.resolver.expand_neighbors(self.ids[0])
        self.assertIsInstance(results, list)

    def test_expand_neighbors_missing_item(self):
        with self.assertRaises(KeyError):
            self.resolver.expand_neighbors("nonexistent_item_xyz")

    def test_expand_neighbors_top_k(self):
        if len(self.ids) < 2:
            self.skipTest("Need at least 2 items")
        results = self.resolver.expand_neighbors(self.ids[0], top_k=1)
        self.assertLessEqual(len(results), 1)


# ---------------------------------------------------------------------------
# Metadata attachment and retrieval
# ---------------------------------------------------------------------------

class TestSemanticResolverMetadata(unittest.TestCase):
    """Test metadata attachment."""

    def setUp(self):
        self.texts = [t for t in SAMPLE_TEXTS if len(t) >= 10]
        self.lsh, self.ids = _build_lsh_with_samples(self.texts)
        self.resolver = SemanticResolver(self.lsh, min_similarity=0.0)
        self.encoder = SignalEncoder(ngram_size=3, num_hashes=128)

    def test_set_metadata_and_retrieve(self):
        if not self.ids:
            self.skipTest("No items inserted into LSH")
        self.resolver.set_metadata(self.ids[0], {"path": "test.py", "type": "py"})
        signal = self.encoder.encode(self.texts[0])
        results = self.resolver.resolve(signal, top_k=20)
        # Find the result for our item
        for r in results:
            if r.item_id == self.ids[0]:
                self.assertIsNotNone(r.metadata)
                self.assertEqual(r.metadata["path"], "test.py")
                break

    def test_metadata_none_by_default(self):
        if not self.ids:
            self.skipTest("No items inserted into LSH")
        signal = self.encoder.encode(self.texts[0])
        results = self.resolver.resolve(signal, top_k=20)
        for r in results:
            if r.item_id == self.ids[0]:
                self.assertIsNone(r.metadata)
                break


# ---------------------------------------------------------------------------
# find_clusters()
# ---------------------------------------------------------------------------

class TestSemanticResolverFindClusters(unittest.TestCase):
    """Test find_clusters() from signal."""

    def setUp(self):
        self.texts = [t for t in SAMPLE_TEXTS if len(t) >= 10]
        self.lsh, self.ids = _build_lsh_with_samples(self.texts)
        self.resolver = SemanticResolver(self.lsh)
        self.encoder = SignalEncoder(ngram_size=3, num_hashes=128)

    def test_find_clusters_returns_list(self):
        if not self.ids:
            self.skipTest("No items inserted into LSH")
        signal = self.encoder.encode(self.texts[0])
        clusters = self.resolver.find_clusters(signal)
        self.assertIsInstance(clusters, list)

    def test_find_clusters_has_members(self):
        if not self.ids:
            self.skipTest("No items inserted into LSH")
        signal = self.encoder.encode(self.texts[0])
        clusters = self.resolver.find_clusters(signal)
        # At least some clusters should have members
        if clusters:
            total_members = sum(len(c) for c in clusters)
            self.assertGreater(total_members, 0)

    def test_find_clusters_each_is_semantic_cluster(self):
        if not self.ids:
            self.skipTest("No items inserted into LSH")
        signal = self.encoder.encode(self.texts[0])
        clusters = self.resolver.find_clusters(signal)
        for c in clusters:
            self.assertIsInstance(c, SemanticCluster)

    def test_find_clusters_empty_minhash(self):
        signal = Signal(
            content_hash=1,
            semantic_fingerprint=0,
            structural_pattern=0,
            query_tokens=(),
            minhash_signature=(),
            shingles=frozenset(),
            timestamp=0.0,
            raw_query="",
        )
        clusters = self.resolver.find_clusters(signal)
        self.assertEqual(clusters, [])


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