"""Tests for rbsp.runtime.distributed -- distributed sharding system."""
import os
import sys
import threading
import unittest

sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))

from rbsp.runtime.distributed import (
    ConsistencyLevel, ShardConfig, ShardRouter, Shard, ShardedIndex,
    ReplicaManager, QueryCoordinator, jump_consistent_hash, _doc_id_to_int,
)


def _make_vector(dim, val):
    return [val] * dim


class TestConsistencyLevel(unittest.TestCase):
    def test_values(self):
        self.assertEqual(ConsistencyLevel.ONE.value, 1)
        self.assertEqual(ConsistencyLevel.QUORUM.value, 2)
        self.assertEqual(ConsistencyLevel.ALL.value, 3)

    def test_member_count(self):
        self.assertEqual(len(list(ConsistencyLevel)), 3)

    def test_names(self):
        self.assertEqual(ConsistencyLevel.ONE.name, "ONE")
        self.assertEqual(ConsistencyLevel.QUORUM.name, "QUORUM")
        self.assertEqual(ConsistencyLevel.ALL.name, "ALL")


class TestJumpConsistentHash(unittest.TestCase):
    def test_single_bucket(self):
        self.assertEqual(jump_consistent_hash(42, 1), 0)

    def test_returns_valid_range(self):
        for key in range(200):
            b = jump_consistent_hash(key, 10)
            self.assertGreaterEqual(b, 0)
            self.assertLess(b, 10)

    def test_deterministic(self):
        self.assertEqual(jump_consistent_hash(12345, 8), jump_consistent_hash(12345, 8))

    def test_uniform(self):
        counts = [0] * 4
        for key in range(1000):
            counts[jump_consistent_hash(key, 4)] += 1
        for c in counts:
            self.assertGreater(c, 100)
            self.assertLess(c, 450)

    def test_minimal_reassignment(self):
        changed = sum(1 for k in range(1000) if jump_consistent_hash(k, 4) != jump_consistent_hash(k, 5))
        self.assertLess(changed, 400)

    def test_zero_raises(self):
        with self.assertRaises(ValueError):
            jump_consistent_hash(0, 0)

    def test_negative_raises(self):
        with self.assertRaises(ValueError):
            jump_consistent_hash(0, -1)


class TestShardConfig(unittest.TestCase):
    def test_defaults(self):
        cfg = ShardConfig()
        self.assertEqual(cfg.num_shards, 4)
        self.assertEqual(cfg.replication_factor, 1)
        self.assertEqual(cfg.consistency_level, ConsistencyLevel.ONE)
        self.assertEqual(cfg.scatter_timeout, 5.0)
        self.assertTrue(cfg.allow_partial)

    def test_custom(self):
        cfg = ShardConfig(num_shards=8, replication_factor=3, consistency_level=ConsistencyLevel.QUORUM)
        self.assertEqual(cfg.num_shards, 8)
        self.assertEqual(cfg.replication_factor, 3)

    def test_invalid_shards(self):
        with self.assertRaises(ValueError):
            ShardConfig(num_shards=0)

    def test_invalid_replication(self):
        with self.assertRaises(ValueError):
            ShardConfig(replication_factor=0)

    def test_invalid_timeout(self):
        with self.assertRaises(ValueError):
            ShardConfig(scatter_timeout=0)

    def test_repr(self):
        self.assertIn("num_shards=2", repr(ShardConfig(num_shards=2)))

    def test_keyword_only(self):
        with self.assertRaises(TypeError):
            ShardConfig(8)

    def test_negative_num_shards(self):
        with self.assertRaises(ValueError):
            ShardConfig(num_shards=-5)

    def test_negative_scatter_timeout(self):
        with self.assertRaises(ValueError):
            ShardConfig(scatter_timeout=-2.0)

    def test_allow_partial_false(self):
        cfg = ShardConfig(allow_partial=False)
        self.assertFalse(cfg.allow_partial)

    def test_consistency_in_repr(self):
        cfg = ShardConfig(consistency_level=ConsistencyLevel.ALL)
        self.assertIn("ALL", repr(cfg))


class TestShardRouter(unittest.TestCase):
    def test_deterministic(self):
        r = ShardRouter(4)
        self.assertEqual(r.route("doc1"), r.route("doc1"))

    def test_valid_range(self):
        r = ShardRouter(4)
        for i in range(100):
            self.assertIn(r.route(f"doc_{i}"), range(4))

    def test_distributes(self):
        r = ShardRouter(4)
        counts = {}
        for i in range(1000):
            s = r.route(f"d_{i}")
            counts[s] = counts.get(s, 0) + 1
        for c in counts.values():
            self.assertGreater(c, 100)
            self.assertLess(c, 450)

    def test_replicas(self):
        shards = ShardRouter(4).route_with_replicas("doc1", 2)
        self.assertEqual(len(shards), 2)
        self.assertEqual(len(set(shards)), 2)

    def test_replicas_capped(self):
        self.assertEqual(len(ShardRouter(3).route_with_replicas("doc1", 5)), 3)

    def test_replicas_single(self):
        self.assertEqual(len(ShardRouter(4).route_with_replicas("doc1", 1)), 1)

    def test_setter(self):
        r = ShardRouter(4)
        r.num_shards = 8
        self.assertEqual(r.num_shards, 8)

    def test_setter_invalid(self):
        r = ShardRouter(4)
        with self.assertRaises(ValueError):
            r.num_shards = 0

    def test_single(self):
        self.assertEqual(ShardRouter(1).route("x"), 0)

    def test_repr(self):
        self.assertIn("ShardRouter", repr(ShardRouter(4)))

    def test_invalid_init(self):
        with self.assertRaises(ValueError):
            ShardRouter(0)

    def test_thread_safety_route(self):
        """Concurrent calls to route should not crash."""
        router = ShardRouter(4)
        errors = []

        def _route_many():
            try:
                for i in range(200):
                    s = router.route(f"doc_{i}")
                    assert 0 <= s < 4
            except Exception as e:
                errors.append(e)

        threads = [threading.Thread(target=_route_many) for _ in range(4)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()
        self.assertEqual(errors, [])

    def test_thread_safety_setter(self):
        """Concurrent reads and writes to num_shards should not crash."""
        router = ShardRouter(4)
        errors = []

        def _resize():
            try:
                for n in [2, 4, 8, 4]:
                    router.num_shards = n
            except Exception as e:
                errors.append(e)

        def _route():
            try:
                for i in range(100):
                    router.route(f"doc_{i}")
            except Exception as e:
                errors.append(e)

        threads = [threading.Thread(target=_resize), threading.Thread(target=_route)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()
        self.assertEqual(errors, [])


class TestShard(unittest.TestCase):
    def test_insert_search(self):
        s = Shard(0)
        s.insert("d1", tokens=["python", "search"])
        self.assertIn("d1", s.search_tokens("python"))

    def test_doc_count(self):
        s = Shard(0)
        self.assertEqual(s.doc_count, 0)
        s.insert("d1", tokens=["a"])
        self.assertEqual(s.doc_count, 1)

    def test_remove(self):
        s = Shard(0)
        s.insert("d1", tokens=["a"])
        self.assertTrue(s.remove("d1"))
        self.assertFalse(s.contains("d1"))

    def test_remove_nonexistent(self):
        self.assertFalse(Shard(0).remove("x"))

    def test_contains(self):
        s = Shard(0)
        self.assertFalse(s.contains("d1"))
        s.insert("d1", tokens=["a"])
        self.assertTrue(s.contains("d1"))

    def test_all_ids(self):
        s = Shard(0)
        s.insert("d1", tokens=["a"])
        s.insert("d2", tokens=["b"])
        self.assertEqual(s.all_doc_ids(), {"d1", "d2"})

    def test_no_match(self):
        s = Shard(0)
        s.insert("d1", tokens=["hello"])
        self.assertEqual(s.search_tokens("python"), set())

    def test_boolean(self):
        s = Shard(0)
        s.insert("d1", tokens=["python", "search"])
        s.insert("d2", tokens=["python", "web"])
        r = s.search_boolean("python AND search")
        self.assertIn("d1", r)
        self.assertNotIn("d2", r)

    def test_vector(self):
        s = Shard(0, vector_dim=4)
        s.insert("d1", vector=_make_vector(4, 1.0))
        s.insert("d2", vector=_make_vector(4, 0.5))
        r = s.search_vector(_make_vector(4, 1.0), k=2, mode="exact")
        self.assertEqual(r[0][0], "d1")

    def test_tokens_only(self):
        s = Shard(0)
        s.insert("d1", tokens=["a"])
        self.assertTrue(s.contains("d1"))

    def test_vector_only(self):
        s = Shard(0, vector_dim=4)
        s.insert("d1", vector=_make_vector(4, 1.0))
        self.assertTrue(s.contains("d1"))

    def test_both(self):
        s = Shard(0, vector_dim=4)
        s.insert("d1", tokens=["hi"], vector=_make_vector(4, 1.0))
        self.assertIn("d1", s.search_tokens("hi"))

    def test_len(self):
        s = Shard(0)
        self.assertEqual(len(s), 0)
        s.insert("d1", tokens=["a"])
        self.assertEqual(len(s), 1)

    def test_contains_dunder(self):
        s = Shard(0)
        self.assertNotIn("d1", s)
        s.insert("d1", tokens=["a"])
        self.assertIn("d1", s)

    def test_repr(self):
        self.assertIn("id=7", repr(Shard(7)))

    def test_inverted(self):
        self.assertIsNotNone(Shard(0).inverted)

    def test_vector_store(self):
        self.assertIsNotNone(Shard(0).vector_store)

    def test_insert_no_tokens_no_vector(self):
        s = Shard(0)
        s.insert("d1")
        self.assertTrue(s.contains("d1"))
        self.assertEqual(s.doc_count, 1)

    def test_all_doc_ids_returns_copy(self):
        s = Shard(0)
        s.insert("d1", tokens=["a"])
        ids = s.all_doc_ids()
        ids.add("fake")
        self.assertNotIn("fake", s.all_doc_ids())

    def test_search_tokens_multiple_docs(self):
        s = Shard(0)
        s.insert("d1", tokens=["python"])
        s.insert("d2", tokens=["python"])
        self.assertEqual(s.search_tokens("python"), {"d1", "d2"})

    def test_shard_id_attribute(self):
        self.assertEqual(Shard(42).shard_id, 42)

    def test_remove_then_insert(self):
        s = Shard(0)
        s.insert("d1", tokens=["a"])
        s.remove("d1")
        s.insert("d1", tokens=["b"])
        self.assertTrue(s.contains("d1"))
        self.assertEqual(s.doc_count, 1)


class TestReplicaManager(unittest.TestCase):
    def test_register_read(self):
        rm = ReplicaManager(ShardConfig())
        s = Shard(0)
        rm.register_shard(s)
        self.assertIs(rm.get_read_shard(0), s)

    def test_add_replica(self):
        rm = ReplicaManager(ShardConfig())
        rm.register_shard(Shard(0))
        self.assertEqual(rm.add_replica(0, Shard(0)), 1)
        self.assertEqual(rm.replica_count(0), 2)

    def test_all_replicas(self):
        rm = ReplicaManager(ShardConfig())
        rm.register_shard(Shard(0))
        rm.add_replica(0, Shard(0))
        self.assertEqual(len(rm.get_all_replicas(0)), 2)

    def test_health(self):
        rm = ReplicaManager(ShardConfig())
        p, r = Shard(0), Shard(0)
        rm.register_shard(p)
        rm.add_replica(0, r)
        rm.mark_unhealthy(0, 0)
        self.assertIs(rm.get_read_shard(0), r)
        rm.mark_healthy(0, 0)
        self.assertIs(rm.get_read_shard(0), p)

    def test_all_unhealthy(self):
        rm = ReplicaManager(ShardConfig())
        rm.register_shard(Shard(0))
        rm.mark_unhealthy(0, 0)
        self.assertIsNone(rm.get_read_shard(0))

    def test_write_one(self):
        rm = ReplicaManager(ShardConfig())
        rm.register_shard(Shard(0))
        rm.add_replica(0, Shard(0))
        self.assertEqual(len(rm.get_write_replicas(0, ConsistencyLevel.ONE)), 1)

    def test_write_quorum(self):
        rm = ReplicaManager(ShardConfig())
        rm.register_shard(Shard(0))
        rm.add_replica(0, Shard(0))
        rm.add_replica(0, Shard(0))
        self.assertEqual(len(rm.get_write_replicas(0, ConsistencyLevel.QUORUM)), 2)

    def test_write_all(self):
        rm = ReplicaManager(ShardConfig())
        rm.register_shard(Shard(0))
        rm.add_replica(0, Shard(0))
        self.assertEqual(len(rm.get_write_replicas(0, ConsistencyLevel.ALL)), 2)

    def test_repr(self):
        self.assertIn("ReplicaManager", repr(ReplicaManager(ShardConfig())))

    def test_unknown(self):
        self.assertIsNone(ReplicaManager(ShardConfig()).get_read_shard(99))

    def test_get_all_replicas_empty(self):
        rm = ReplicaManager(ShardConfig())
        self.assertEqual(rm.get_all_replicas(99), [])

    def test_replica_count_no_shard(self):
        rm = ReplicaManager(ShardConfig())
        self.assertEqual(rm.replica_count(99), 0)

    def test_register_replaces_primary(self):
        rm = ReplicaManager(ShardConfig())
        s1 = Shard(0)
        s2 = Shard(0)
        rm.register_shard(s1)
        rm.register_shard(s2)
        self.assertIs(rm.get_read_shard(0), s2)

    def test_write_replicas_unhealthy_excluded(self):
        rm = ReplicaManager(ShardConfig())
        rm.register_shard(Shard(0))
        rm.add_replica(0, Shard(0))
        rm.mark_unhealthy(0, 1)
        targets = rm.get_write_replicas(0, ConsistencyLevel.ALL)
        self.assertEqual(len(targets), 1)


class TestQueryCoordinator(unittest.TestCase):
    def test_tokens(self):
        c = QueryCoordinator(ShardConfig())
        ss = [Shard(i) for i in range(3)]
        ss[0].insert("d1", tokens=["python", "search"])
        ss[1].insert("d2", tokens=["python", "web"])
        ss[2].insert("d3", tokens=["java"])
        self.assertEqual(c.scatter_gather_tokens(ss, "python"), {"d1", "d2"})

    def test_boolean(self):
        c = QueryCoordinator(ShardConfig())
        ss = [Shard(i) for i in range(2)]
        ss[0].insert("d1", tokens=["python", "search"])
        ss[1].insert("d2", tokens=["python", "web"])
        r = c.scatter_gather_boolean(ss, "python AND search")
        self.assertIn("d1", r)
        self.assertNotIn("d2", r)

    def test_vector(self):
        c = QueryCoordinator(ShardConfig())
        ss = [Shard(i, vector_dim=4) for i in range(2)]
        ss[0].insert("d1", vector=_make_vector(4, 1.0))
        ss[1].insert("d2", vector=_make_vector(4, 0.5))
        r = c.scatter_gather_vector(ss, _make_vector(4, 1.0), k=2, mode="exact")
        self.assertEqual(r[0][0], "d1")

    def test_empty(self):
        c = QueryCoordinator(ShardConfig())
        self.assertEqual(c.scatter_gather_tokens([], "x"), set())
        self.assertEqual(c.scatter_gather_boolean([], "x"), set())
        self.assertEqual(c.scatter_gather_vector([], [1.0], k=5), [])

    def test_vector_topk(self):
        c = QueryCoordinator(ShardConfig())
        ss = [Shard(i, vector_dim=4) for i in range(2)]
        for j in range(5):
            ss[0].insert(f"a{j}", vector=_make_vector(4, float(j)))
            ss[1].insert(f"b{j}", vector=_make_vector(4, float(j + 5)))
        self.assertLessEqual(len(c.scatter_gather_vector(ss, _make_vector(4, 0.0), k=3, mode="exact")), 3)


class TestShardedIndex(unittest.TestCase):
    def test_insert_search(self):
        idx = ShardedIndex(ShardConfig(num_shards=4))
        idx.insert("d1", tokens=["python", "search"])
        idx.insert("d2", tokens=["python", "web"])
        idx.insert("d3", tokens=["java"])
        r = idx.search_tokens("python")
        self.assertIn("d1", r)
        self.assertIn("d2", r)
        self.assertNotIn("d3", r)

    def test_boolean(self):
        idx = ShardedIndex(ShardConfig(num_shards=2))
        idx.insert("d1", tokens=["python", "search"])
        idx.insert("d2", tokens=["python", "web"])
        self.assertIn("d1", idx.search_boolean("python AND search"))

    def test_total(self):
        idx = ShardedIndex(ShardConfig(num_shards=2))
        self.assertEqual(idx.total_docs, 0)
        idx.insert("d1", tokens=["a"])
        self.assertEqual(idx.total_docs, 1)

    def test_remove(self):
        idx = ShardedIndex(ShardConfig(num_shards=2))
        idx.insert("d1", tokens=["a"])
        self.assertTrue(idx.remove("d1"))
        self.assertNotIn("d1", idx)

    def test_remove_nonexistent(self):
        self.assertFalse(ShardedIndex(ShardConfig(num_shards=2)).remove("x"))

    def test_contains(self):
        idx = ShardedIndex(ShardConfig(num_shards=4))
        idx.insert("d1", tokens=["a"])
        self.assertIn("d1", idx)
        self.assertNotIn("d2", idx)

    def test_shard_sizes(self):
        idx = ShardedIndex(ShardConfig(num_shards=4))
        for i in range(20):
            idx.insert(f"d_{i}", tokens=["c"])
        self.assertEqual(sum(idx.shard_sizes().values()), 20)

    def test_get_shard_for_doc(self):
        idx = ShardedIndex(ShardConfig(num_shards=4))
        self.assertIn(idx.get_shard_for_doc("d1"), range(4))

    def test_get_shard(self):
        idx = ShardedIndex(ShardConfig(num_shards=4))
        self.assertIsNotNone(idx.get_shard(0))
        self.assertIsNone(idx.get_shard(99))

    def test_batch(self):
        idx = ShardedIndex(ShardConfig(num_shards=2))
        c = idx.batch_insert(["d1", "d2", "d3"], tokens_list=[["a"], ["b"], ["c"]])
        self.assertEqual(sum(c.values()), 3)

    def test_batch_vectors(self):
        idx = ShardedIndex(ShardConfig(num_shards=2), vector_dim=4)
        c = idx.batch_insert(["d1", "d2"], tokens_list=[["a"], ["b"]], vectors=[_make_vector(4, 1.0), _make_vector(4, 0.5)])
        self.assertEqual(sum(c.values()), 2)

    def test_vector_search(self):
        idx = ShardedIndex(ShardConfig(num_shards=2), vector_dim=4)
        idx.insert("d1", vector=_make_vector(4, 1.0))
        idx.insert("d2", vector=_make_vector(4, 0.5))
        self.assertEqual(idx.search_vector(_make_vector(4, 1.0), k=2, mode="exact")[0][0], "d1")

    def test_rebalance(self):
        idx = ShardedIndex(ShardConfig(num_shards=2))
        for i in range(20):
            idx.insert(f"d_{i}", tokens=[f"t_{i}", "c"])
        s = idx.rebalance(4)
        self.assertEqual(s["total"], 20)
        self.assertEqual(s["new_shards"], 4)
        self.assertEqual(idx.num_shards, 4)
        self.assertEqual(idx.total_docs, 20)

    def test_rebalance_same(self):
        idx = ShardedIndex(ShardConfig(num_shards=2))
        idx.insert("d1", tokens=["a"])
        self.assertEqual(idx.rebalance(2)["moved"], 0)

    def test_rebalance_invalid(self):
        with self.assertRaises(ValueError):
            ShardedIndex(ShardConfig(num_shards=2)).rebalance(0)

    def test_single_shard(self):
        idx = ShardedIndex(ShardConfig(num_shards=1))
        idx.insert("d1", tokens=["hello"])
        self.assertIn("d1", idx.search_tokens("hello"))

    def test_many(self):
        idx = ShardedIndex(ShardConfig(num_shards=4))
        for i in range(100):
            idx.insert(f"d_{i}", tokens=["common"])
        self.assertEqual(idx.total_docs, 100)
        self.assertEqual(len(idx.search_tokens("common")), 100)

    def test_replicas(self):
        idx = ShardedIndex(ShardConfig(num_shards=2, replication_factor=2))
        idx.insert("d1", tokens=["a"])
        self.assertIn("d1", idx)

    def test_empty_search(self):
        self.assertEqual(ShardedIndex(ShardConfig(num_shards=4)).search_tokens("x"), set())

    def test_len(self):
        idx = ShardedIndex(ShardConfig(num_shards=2))
        self.assertEqual(len(idx), 0)
        idx.insert("d1", tokens=["a"])
        self.assertEqual(len(idx), 1)

    def test_repr(self):
        self.assertIn("shards=2", repr(ShardedIndex(ShardConfig(num_shards=2))))

    def test_num_shards(self):
        self.assertEqual(ShardedIndex(ShardConfig(num_shards=3)).num_shards, 3)

    def test_config(self):
        cfg = ShardConfig(num_shards=2)
        self.assertIs(ShardedIndex(cfg).config, cfg)

    def test_reindex(self):
        idx = ShardedIndex(ShardConfig(num_shards=2))
        idx.insert("d1", tokens=["a"])
        idx.insert("d1", tokens=["b"])
        self.assertEqual(idx.get_shard(idx.get_shard_for_doc("d1")).doc_count, 1)

    def test_default_config(self):
        idx = ShardedIndex()
        self.assertEqual(idx.num_shards, 4)

    def test_insert_returns_shard_id(self):
        idx = ShardedIndex(ShardConfig(num_shards=4))
        sid = idx.insert("d1", tokens=["hello"])
        self.assertGreaterEqual(sid, 0)
        self.assertLess(sid, 4)

    def test_insert_doc_routed_correctly(self):
        idx = ShardedIndex(ShardConfig(num_shards=4))
        sid = idx.insert("doc_abc", tokens=["hello"])
        shard = idx.get_shard(sid)
        self.assertTrue(shard.contains("doc_abc"))

    def test_rebalance_preserves_search(self):
        idx = ShardedIndex(ShardConfig(num_shards=2))
        idx.insert("d1", tokens=["python", "search"])
        idx.insert("d2", tokens=["java", "search"])
        idx.rebalance(4)
        r = idx.search_tokens("search")
        self.assertIn("d1", r)
        self.assertIn("d2", r)

    def test_rebalance_decrease(self):
        idx = ShardedIndex(ShardConfig(num_shards=4))
        for i in range(20):
            idx.insert(f"d_{i}", tokens=["c"])
        idx.rebalance(2)
        self.assertEqual(idx.num_shards, 2)
        self.assertEqual(idx.total_docs, 20)

    def test_remove_from_replicas(self):
        cfg = ShardConfig(num_shards=2, replication_factor=2)
        idx = ShardedIndex(cfg)
        idx.insert("d1", tokens=["hello"])
        self.assertIn("d1", idx)
        idx.remove("d1")
        self.assertNotIn("d1", idx)
        self.assertEqual(idx.total_docs, 0)

    def test_batch_insert_no_tokens(self):
        idx = ShardedIndex(ShardConfig(num_shards=2))
        c = idx.batch_insert(["d1", "d2", "d3"])
        self.assertEqual(sum(c.values()), 3)
        self.assertEqual(idx.total_docs, 3)

    def test_search_boolean_empty(self):
        idx = ShardedIndex(ShardConfig(num_shards=2))
        self.assertEqual(idx.search_boolean("python"), set())

    def test_get_shard_for_doc_deterministic(self):
        idx = ShardedIndex(ShardConfig(num_shards=4))
        self.assertEqual(idx.get_shard_for_doc("x"), idx.get_shard_for_doc("x"))

    def test_thread_safety_insert(self):
        """Concurrent inserts should not crash."""
        idx = ShardedIndex(ShardConfig(num_shards=4))
        errors = []

        def _insert_many(prefix):
            try:
                for i in range(50):
                    idx.insert(f"{prefix}_{i}", tokens=["hello", f"t_{i}"])
            except Exception as e:
                errors.append(e)

        threads = [
            threading.Thread(target=_insert_many, args=(f"t{t}",))
            for t in range(4)
        ]
        for t in threads:
            t.start()
        for t in threads:
            t.join()
        self.assertEqual(errors, [])
        self.assertEqual(idx.total_docs, 200)


class TestDocIdToInt(unittest.TestCase):
    def test_int(self):
        self.assertIsInstance(_doc_id_to_int("hello"), int)

    def test_non_negative(self):
        for s in ["hello", "world", ""]:
            self.assertGreaterEqual(_doc_id_to_int(s), 0)

    def test_deterministic(self):
        self.assertEqual(_doc_id_to_int("test"), _doc_id_to_int("test"))


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