"""Tests for rbsp.core.roaring -- RoaringBitmap."""

import os
import sys
import unittest

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

from rbsp.core.roaring import RoaringBitmap, _ArrayContainer, _BitsetContainer


class TestRoaringBasic(RBSPTestCase):
    """Basic add / contains / remove semantics."""

    def test_empty_bitmap(self):
        bm = RoaringBitmap()
        self.assertEqual(len(bm), 0)
        self.assertFalse(bm)
        self.assertNotIn(0, bm)

    def test_add_and_contains(self):
        bm = RoaringBitmap()
        bm.add(42)
        self.assertIn(42, bm)
        self.assertNotIn(43, bm)
        self.assertEqual(len(bm), 1)

    def test_add_duplicate(self):
        bm = RoaringBitmap()
        bm.add(10)
        bm.add(10)
        self.assertEqual(len(bm), 1)

    def test_remove_existing(self):
        bm = RoaringBitmap()
        bm.add(5)
        bm.remove(5)
        self.assertNotIn(5, bm)
        self.assertEqual(len(bm), 0)

    def test_remove_missing_raises(self):
        bm = RoaringBitmap()
        with self.assertRaises(KeyError):
            bm.remove(999)

    def test_discard_missing_no_error(self):
        bm = RoaringBitmap()
        bm.discard(999)  # should not raise

    def test_bool_truthy(self):
        bm = RoaringBitmap()
        self.assertFalse(bm)
        bm.add(1)
        self.assertTrue(bm)

    def test_contains_non_int(self):
        bm = RoaringBitmap()
        bm.add(1)
        self.assertNotIn("1", bm)

    def test_contains_out_of_range(self):
        bm = RoaringBitmap()
        bm.add(1)
        self.assertNotIn(-1, bm)
        self.assertNotIn(0x100000000, bm)

    def test_add_negative_raises(self):
        bm = RoaringBitmap()
        with self.assertRaises(ValueError):
            bm.add(-1)

    def test_add_too_large_raises(self):
        bm = RoaringBitmap()
        with self.assertRaises(ValueError):
            bm.add(0x100000000)


class TestRoaringIteration(RBSPTestCase):
    """Iteration and ordering."""

    def test_sorted_iteration(self):
        bm = RoaringBitmap()
        values = [100, 3, 50, 7, 99]
        for v in values:
            bm.add(v)
        self.assertEqual(list(bm), sorted(values))

    def test_multi_chunk_iteration(self):
        bm = RoaringBitmap()
        bm.add(0)
        bm.add(65536)
        bm.add(131072)
        self.assertEqual(list(bm), [0, 65536, 131072])

    def test_min_max(self):
        bm = RoaringBitmap.from_iterable([10, 20, 30])
        self.assertEqual(bm.min(), 10)
        self.assertEqual(bm.max(), 30)

    def test_min_max_empty_raises(self):
        bm = RoaringBitmap()
        with self.assertRaises(ValueError):
            bm.min()
        with self.assertRaises(ValueError):
            bm.max()

    def test_min_max_multi_chunk(self):
        bm = RoaringBitmap()
        bm.add(5)
        bm.add(200000)
        self.assertEqual(bm.min(), 5)
        self.assertEqual(bm.max(), 200000)


class TestRoaringSetOperations(RBSPTestCase):
    """Intersection, union, difference."""

    def setUp(self):
        super().setUp()
        self.a = RoaringBitmap.from_iterable(range(100))
        self.b = RoaringBitmap.from_iterable(range(50, 150))

    def test_intersection(self):
        result = self.a & self.b
        self.assertEqual(len(result), 50)
        self.assertEqual(list(result), list(range(50, 100)))

    def test_union(self):
        result = self.a | self.b
        self.assertEqual(len(result), 150)
        self.assertEqual(list(result), list(range(150)))

    def test_difference(self):
        result = self.a - self.b
        self.assertEqual(len(result), 50)
        self.assertEqual(list(result), list(range(50)))

    def test_intersection_disjoint(self):
        c = RoaringBitmap.from_iterable(range(200, 300))
        result = self.a & c
        self.assertEqual(len(result), 0)

    def test_intersection_empty(self):
        result = self.a & RoaringBitmap()
        self.assertEqual(len(result), 0)

    def test_union_empty(self):
        result = self.a | RoaringBitmap()
        self.assertEqual(list(result), list(range(100)))

    def test_difference_empty(self):
        result = self.a - RoaringBitmap()
        self.assertEqual(list(result), list(range(100)))

    def test_not_implemented_for_non_roaring(self):
        self.assertEqual(self.a.__and__(42), NotImplemented)
        self.assertEqual(self.a.__or__(42), NotImplemented)
        self.assertEqual(self.a.__sub__(42), NotImplemented)


class TestRoaringCrossChunkOps(RBSPTestCase):
    """Set operations spanning multiple chunks (high-16 values)."""

    def test_cross_chunk_intersection(self):
        a = RoaringBitmap.from_iterable(range(0, 70000))
        b = RoaringBitmap.from_iterable(range(60000, 130000))
        result = a & b
        self.assertEqual(len(result), 10000)

    def test_cross_chunk_union(self):
        a = RoaringBitmap.from_iterable(range(0, 70000))
        b = RoaringBitmap.from_iterable(range(60000, 130000))
        result = a | b
        self.assertEqual(len(result), 130000)

    def test_cross_chunk_difference(self):
        a = RoaringBitmap.from_iterable(range(0, 70000))
        b = RoaringBitmap.from_iterable(range(60000, 130000))
        result = a - b
        self.assertEqual(len(result), 60000)


class TestRoaringContainerPromotion(RBSPTestCase):
    """Array->bitset promotion and bitset->array demotion."""

    def test_array_to_bitset_promotion(self):
        bm = RoaringBitmap()
        for i in range(5000):
            bm.add(i)
        # All in chunk 0, should have been promoted to bitset
        container = bm._containers[0]
        self.assertIsInstance(container, _BitsetContainer)
        self.assertEqual(len(bm), 5000)

    def test_stays_array_below_threshold(self):
        bm = RoaringBitmap()
        for i in range(4095):
            bm.add(i)
        container = bm._containers[0]
        self.assertIsInstance(container, _ArrayContainer)

    def test_bitset_to_array_demotion(self):
        bm = RoaringBitmap()
        for i in range(5000):
            bm.add(i)
        self.assertIsInstance(bm._containers[0], _BitsetContainer)
        # Remove enough to go below threshold
        for i in range(4095, 5000):
            bm.remove(i)
        # Now at 4095, still bitset
        for i in range(4000, 4095):
            bm.remove(i)
        # Now at 4000 < 4096, should have demoted
        self.assertIsInstance(bm._containers[0], _ArrayContainer)
        self.assertEqual(len(bm), 4000)

    def test_empty_container_removed(self):
        bm = RoaringBitmap()
        bm.add(42)
        bm.remove(42)
        self.assertEqual(len(bm._containers), 0)


class TestRoaringSerialization(RBSPTestCase):
    """to_bytes / from_bytes roundtrip."""

    def test_empty_roundtrip(self):
        bm = RoaringBitmap()
        data = bm.to_bytes()
        bm2 = RoaringBitmap.from_bytes(data)
        self.assertEqual(len(bm2), 0)

    def test_array_container_roundtrip(self):
        bm = RoaringBitmap.from_iterable(range(100))
        data = bm.to_bytes()
        bm2 = RoaringBitmap.from_bytes(data)
        self.assertEqual(bm, bm2)

    def test_bitset_container_roundtrip(self):
        bm = RoaringBitmap.from_iterable(range(5000))
        data = bm.to_bytes()
        bm2 = RoaringBitmap.from_bytes(data)
        self.assertEqual(bm, bm2)
        self.assertEqual(len(bm2), 5000)

    def test_multi_chunk_roundtrip(self):
        bm = RoaringBitmap()
        bm.add(0)
        bm.add(65536)
        bm.add(200000)
        data = bm.to_bytes()
        bm2 = RoaringBitmap.from_bytes(data)
        self.assertEqual(list(bm2), [0, 65536, 200000])

    def test_large_roundtrip(self):
        bm = RoaringBitmap.from_iterable(range(0, 100000, 3))
        data = bm.to_bytes()
        bm2 = RoaringBitmap.from_bytes(data)
        self.assertEqual(bm, bm2)

    def test_from_bytes_too_short(self):
        with self.assertRaises(ValueError):
            RoaringBitmap.from_bytes(b"\x00")

    def test_from_bytes_truncated_header(self):
        bm = RoaringBitmap.from_iterable([1, 2, 3])
        data = bm.to_bytes()
        with self.assertRaises(ValueError):
            RoaringBitmap.from_bytes(data[:6])


class TestRoaringEquality(RBSPTestCase):
    """Equality comparisons."""

    def test_equal_bitmaps(self):
        a = RoaringBitmap.from_iterable([1, 2, 3])
        b = RoaringBitmap.from_iterable([1, 2, 3])
        self.assertEqual(a, b)

    def test_unequal_bitmaps(self):
        a = RoaringBitmap.from_iterable([1, 2, 3])
        b = RoaringBitmap.from_iterable([1, 2, 4])
        self.assertNotEqual(a, b)

    def test_unequal_different_length(self):
        a = RoaringBitmap.from_iterable([1, 2])
        b = RoaringBitmap.from_iterable([1, 2, 3])
        self.assertNotEqual(a, b)

    def test_not_equal_to_non_roaring(self):
        a = RoaringBitmap.from_iterable([1, 2, 3])
        self.assertNotEqual(a, {1, 2, 3})


class TestRoaringRepr(RBSPTestCase):
    """String representation."""

    def test_repr(self):
        bm = RoaringBitmap.from_iterable([1, 2, 3])
        r = repr(bm)
        self.assertIn("cardinality=3", r)
        self.assertIn("containers=1", r)


class TestRoaringFromIterable(RBSPTestCase):
    """Convenience constructor."""

    def test_from_iterable(self):
        bm = RoaringBitmap.from_iterable(range(10))
        self.assertEqual(list(bm), list(range(10)))

    def test_from_iterable_empty(self):
        bm = RoaringBitmap.from_iterable(iter([]))
        self.assertEqual(len(bm), 0)


class TestArrayContainer(RBSPTestCase):
    """Direct array container tests."""

    def test_add_returns_true_for_new(self):
        c = _ArrayContainer()
        self.assertTrue(c.add(5))

    def test_add_returns_false_for_duplicate(self):
        c = _ArrayContainer()
        c.add(5)
        self.assertFalse(c.add(5))

    def test_remove_returns_true(self):
        c = _ArrayContainer()
        c.add(5)
        self.assertTrue(c.remove(5))

    def test_remove_returns_false_for_missing(self):
        c = _ArrayContainer()
        self.assertFalse(c.remove(5))

    def test_sorted_order(self):
        c = _ArrayContainer()
        for v in [30, 10, 20]:
            c.add(v)
        self.assertEqual(list(c), [10, 20, 30])


class TestBitsetContainer(RBSPTestCase):
    """Direct bitset container tests."""

    def test_add_returns_true_for_new(self):
        c = _BitsetContainer()
        self.assertTrue(c.add(100))

    def test_add_returns_false_for_duplicate(self):
        c = _BitsetContainer()
        c.add(100)
        self.assertFalse(c.add(100))

    def test_remove_returns_true(self):
        c = _BitsetContainer()
        c.add(100)
        self.assertTrue(c.remove(100))

    def test_remove_returns_false_for_missing(self):
        c = _BitsetContainer()
        self.assertFalse(c.remove(100))

    def test_iteration_sorted(self):
        c = _BitsetContainer()
        for v in [300, 100, 200]:
            c.add(v)
        self.assertEqual(list(c), [100, 200, 300])

    def test_cardinality(self):
        c = _BitsetContainer()
        for v in range(1000):
            c.add(v)
        self.assertEqual(len(c), 1000)


class TestMixedContainerOps(RBSPTestCase):
    """Set operations between array and bitset containers."""

    def test_bitset_and_array_intersection(self):
        # Build one sparse, one dense bitmap in same chunk
        a = RoaringBitmap.from_iterable(range(5000))   # bitset
        b = RoaringBitmap.from_iterable(range(100))     # array
        result = a & b
        self.assertEqual(len(result), 100)

    def test_bitset_or_array_union(self):
        a = RoaringBitmap.from_iterable(range(5000))
        b = RoaringBitmap.from_iterable(range(4990, 5100))
        result = a | b
        self.assertEqual(len(result), 5100)

    def test_array_minus_bitset(self):
        a = RoaringBitmap.from_iterable(range(100))      # array
        b = RoaringBitmap.from_iterable(range(5000))      # bitset
        result = a - b
        self.assertEqual(len(result), 0)

    def test_bitset_minus_array(self):
        a = RoaringBitmap.from_iterable(range(5000))      # bitset
        b = RoaringBitmap.from_iterable(range(100))        # array
        result = a - b
        self.assertEqual(len(result), 4900)


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