"""Tests for rbsp.indexer.clusters -- SemanticClusterEngine."""

import os
import sys
import unittest

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

from rbsp.indexer.clusters import SemanticClusterEngine


class TestSemanticClusterEngineBasics(unittest.TestCase):
    """Core add/remove/lookup semantics."""

    def setUp(self):
        self.engine = SemanticClusterEngine()

    def test_empty_engine(self):
        self.assertEqual(len(self.engine), 0)
        self.assertEqual(self.engine.item_count(), 0)
        self.assertEqual(self.engine.bucket_count(), 0)

    def test_add_single_item(self):
        self.engine.add("item1", [10, 20, 30])
        self.assertEqual(self.engine.item_count(), 1)
        self.assertIn("item1", self.engine)

    def test_add_multiple_items(self):
        self.engine.add("a", [1, 2])
        self.engine.add("b", [3, 4])
        self.engine.add("c", [5, 6])
        self.assertEqual(self.engine.item_count(), 3)
        self.assertEqual(len(self.engine), 3)

    def test_remove_existing(self):
        self.engine.add("item1", [10, 20])
        result = self.engine.remove("item1")
        self.assertTrue(result)
        self.assertNotIn("item1", self.engine)
        self.assertEqual(self.engine.item_count(), 0)

    def test_remove_nonexistent(self):
        result = self.engine.remove("nope")
        self.assertFalse(result)

    def test_remove_cleans_buckets(self):
        self.engine.add("item1", [10])
        self.engine.remove("item1")
        self.assertEqual(self.engine.bucket_count(), 0)
        self.assertEqual(self.engine.cluster_size(10), 0)

    def test_contains(self):
        self.engine.add("x", [1])
        self.assertIn("x", self.engine)
        self.assertNotIn("y", self.engine)


class TestSemanticClusterEngineGetCluster(unittest.TestCase):
    """Tests for get_cluster()."""

    def setUp(self):
        self.engine = SemanticClusterEngine()

    def test_get_cluster(self):
        self.engine.add("a", [10, 20])
        self.engine.add("b", [10, 30])
        self.engine.add("c", [20, 30])
        cluster_10 = self.engine.get_cluster(10)
        self.assertEqual(cluster_10, {"a", "b"})

    def test_get_cluster_empty_bucket(self):
        cluster = self.engine.get_cluster(999)
        self.assertEqual(cluster, set())

    def test_get_cluster_returns_copy(self):
        self.engine.add("item1", [10])
        cluster = self.engine.get_cluster(10)
        cluster.add("injected")
        self.assertNotIn("injected", self.engine.get_cluster(10))


class TestSemanticClusterEngineItemBuckets(unittest.TestCase):
    """Tests for get_item_buckets()."""

    def setUp(self):
        self.engine = SemanticClusterEngine()

    def test_get_item_buckets(self):
        self.engine.add("item1", [10, 20, 30])
        buckets = self.engine.get_item_buckets("item1")
        self.assertEqual(buckets, frozenset({10, 20, 30}))

    def test_get_item_buckets_nonexistent(self):
        buckets = self.engine.get_item_buckets("nope")
        self.assertEqual(buckets, frozenset())

    def test_get_item_buckets_returns_frozenset(self):
        self.engine.add("item1", [5])
        buckets = self.engine.get_item_buckets("item1")
        self.assertIsInstance(buckets, frozenset)


class TestSemanticClusterEngineNeighbours(unittest.TestCase):
    """Tests for get_neighbours() and get_neighbours_ranked()."""

    def setUp(self):
        self.engine = SemanticClusterEngine()
        # a and b share buckets 10, 20
        # a and c share bucket 10
        # b and c share bucket 30
        self.engine.add("a", [10, 20])
        self.engine.add("b", [10, 20, 30])
        self.engine.add("c", [10, 30])

    def test_get_neighbours(self):
        neighbours = self.engine.get_neighbours("a")
        self.assertIn("b", neighbours)
        self.assertIn("c", neighbours)
        self.assertNotIn("a", neighbours)

    def test_get_neighbours_include_self(self):
        neighbours = self.engine.get_neighbours("a", exclude_self=False)
        self.assertIn("a", neighbours)
        self.assertIn("b", neighbours)

    def test_get_neighbours_nonexistent(self):
        result = self.engine.get_neighbours("nope")
        self.assertEqual(result, set())

    def test_get_neighbours_ranked(self):
        ranked = self.engine.get_neighbours_ranked("a")
        # b shares 2 buckets (10, 20), c shares 1 bucket (10)
        self.assertEqual(ranked[0][0], "b")
        self.assertEqual(ranked[0][1], 2)
        self.assertEqual(ranked[1][0], "c")
        self.assertEqual(ranked[1][1], 1)

    def test_get_neighbours_ranked_top_k(self):
        ranked = self.engine.get_neighbours_ranked("a", top_k=1)
        self.assertEqual(len(ranked), 1)
        self.assertEqual(ranked[0][0], "b")

    def test_get_neighbours_ranked_nonexistent(self):
        result = self.engine.get_neighbours_ranked("nope")
        self.assertEqual(result, [])

    def test_get_neighbours_ranked_exclude_self(self):
        ranked = self.engine.get_neighbours_ranked("a", exclude_self=True)
        ids = [r[0] for r in ranked]
        self.assertNotIn("a", ids)


class TestSemanticClusterEngineQueryByBuckets(unittest.TestCase):
    """Tests for query_by_buckets()."""

    def setUp(self):
        self.engine = SemanticClusterEngine()
        self.engine.add("a", [10, 20])
        self.engine.add("b", [10, 30])
        self.engine.add("c", [20, 30])

    def test_query_by_buckets(self):
        results = self.engine.query_by_buckets([10, 20])
        result_ids = [r[0] for r in results]
        # a matches both (10, 20) -> 2 hits, b matches 10 -> 1, c matches 20 -> 1
        self.assertEqual(results[0], ("a", 2))
        self.assertIn("b", result_ids)
        self.assertIn("c", result_ids)

    def test_query_by_buckets_top_k(self):
        results = self.engine.query_by_buckets([10, 20], top_k=1)
        self.assertEqual(len(results), 1)
        self.assertEqual(results[0][0], "a")

    def test_query_by_buckets_no_match(self):
        results = self.engine.query_by_buckets([999])
        self.assertEqual(results, [])

    def test_query_by_empty_buckets(self):
        results = self.engine.query_by_buckets([])
        self.assertEqual(results, [])


class TestSemanticClusterEngineInfo(unittest.TestCase):
    """Tests for info methods: item_count, bucket_count, cluster_size."""

    def setUp(self):
        self.engine = SemanticClusterEngine()

    def test_item_count(self):
        self.assertEqual(self.engine.item_count(), 0)
        self.engine.add("a", [1])
        self.assertEqual(self.engine.item_count(), 1)

    def test_bucket_count(self):
        self.engine.add("a", [10, 20, 30])
        self.assertEqual(self.engine.bucket_count(), 3)

    def test_cluster_size(self):
        self.engine.add("a", [10])
        self.engine.add("b", [10])
        self.engine.add("c", [20])
        self.assertEqual(self.engine.cluster_size(10), 2)
        self.assertEqual(self.engine.cluster_size(20), 1)
        self.assertEqual(self.engine.cluster_size(999), 0)


class TestSemanticClusterEngineClear(unittest.TestCase):
    """Tests for clear()."""

    def test_clear(self):
        engine = SemanticClusterEngine()
        engine.add("a", [1, 2])
        engine.add("b", [3, 4])
        engine.clear()
        self.assertEqual(engine.item_count(), 0)
        self.assertEqual(engine.bucket_count(), 0)
        self.assertEqual(len(engine), 0)
        self.assertNotIn("a", engine)


class TestSemanticClusterEngineEdgeCases(unittest.TestCase):
    """Edge cases: update buckets, add/remove same item, etc."""

    def setUp(self):
        self.engine = SemanticClusterEngine()

    def test_update_buckets_replaces_old(self):
        self.engine.add("item1", [10, 20])
        self.engine.add("item1", [30, 40])
        self.assertEqual(self.engine.get_item_buckets("item1"), frozenset({30, 40}))
        # Old buckets should be cleaned
        self.assertEqual(self.engine.get_cluster(10), set())
        self.assertEqual(self.engine.get_cluster(20), set())
        # New buckets should be populated
        self.assertIn("item1", self.engine.get_cluster(30))
        self.assertIn("item1", self.engine.get_cluster(40))

    def test_update_preserves_item_count(self):
        self.engine.add("item1", [10])
        self.engine.add("item1", [20])
        self.assertEqual(self.engine.item_count(), 1)

    def test_add_remove_add(self):
        self.engine.add("item1", [10])
        self.engine.remove("item1")
        self.engine.add("item1", [20])
        self.assertIn("item1", self.engine)
        self.assertEqual(self.engine.get_item_buckets("item1"), frozenset({20}))

    def test_add_empty_buckets(self):
        self.engine.add("item1", [])
        self.assertIn("item1", self.engine)
        self.assertEqual(self.engine.get_item_buckets("item1"), frozenset())
        self.assertEqual(self.engine.bucket_count(), 0)

    def test_single_item_no_neighbours(self):
        self.engine.add("lonely", [100])
        neighbours = self.engine.get_neighbours("lonely")
        self.assertEqual(neighbours, set())


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