#!/usr/bin/env python

# THIS FILE WAS GENERATED BY A LLM.
# The tests help improve coverage but are not hand crafted.

import sys

sys.path.insert(0, "..")
import pytest
from tibs import BitOrder, ByteOrder, Codec, Dtype, DtypeKind, MutableView, Tibs, Mutibs, View


class TestTibsCreation:
    def test_from_bin_simple(self):
        assert Tibs.from_bin("101") == Tibs('0b101')

    def test_from_bin_with_prefix(self):
        assert Tibs.from_bin("0b110") == Tibs('0b110')

    def test_from_oct_simple(self):
        assert Tibs.from_oct("75") == Tibs('0o75')

    def test_from_oct_with_prefix(self):
        assert Tibs.from_oct("0o64") == Tibs('0o64')

    def test_from_hex_simple(self):
        assert Tibs.from_hex("af") == Tibs('0xaf')

    def test_from_hex_with_prefix(self):
        assert Tibs.from_hex("0xbe") == Tibs('0xbe')

    def test_from_u_basic(self):
        assert Tibs.from_u(42, 8) == Tibs('0b00101010')

    def test_from_i_positive(self):
        assert Tibs.from_i(42, 8) == Tibs('0b00101010')

    def test_from_i_negative(self):
        assert Tibs.from_i(-42, 8) == Tibs('0b11010110')

    def test_from_f16(self):
        assert len(Tibs.from_f(3.14, 16)) == 16

    def test_from_f32(self):
        assert len(Tibs.from_f(3.14, 32)) == 32

    def test_from_f64(self):
        assert len(Tibs.from_f(3.14, 64)) == 64

    def test_from_bytes(self):
        assert Tibs.from_bytes(b"abc") == Tibs('0x616263')

    def test_from_string_bin(self):
        assert Tibs.from_string("0b101") == Tibs('0b101')

    def test_from_string_hex(self):
        assert Tibs.from_string("0xabc") == Tibs('0xabc')

    def test_from_bools(self):
        assert Tibs.from_bools([True, False, 1, 0]) == Tibs('0b1010')

    def test_from_zeros(self):
        assert Tibs.from_zeros(5) == Tibs('0b00000')

    def test_from_ones(self):
        assert Tibs.from_ones(5) == Tibs('0b11111')

    def test_from_random(self):
        assert len(Tibs.from_random(100)) == 100

    def test_from_joined(self):
        assert Tibs.from_joined(["0b11", "0o7", "0xf"]) == Tibs('0b111111111')

    def test_constructor_delegation(self):
        assert Tibs("0b101") == Tibs.from_string("0b101")
        assert Tibs(b"abc") == Tibs.from_bytes(b"abc")
        assert Tibs([True, False]) == Tibs.from_bools([True, False])

    def test_from_slice(self):
        a = Tibs("0b101100111000")
        b = a[2:10]
        assert b == Tibs('0b11001110')
        c = Tibs(b)
        assert c == Tibs('0b11001110')


class TestMutibsCreation:
    def test_from_bin_simple(self):
        assert Mutibs.from_bin("101") == Tibs('0b101')

    def test_from_oct_simple(self):
        assert Mutibs.from_oct("75") == Tibs('0o75')

    def test_from_hex_simple(self):
        assert Mutibs.from_hex("af") == Tibs('0xaf')

    def test_from_u_basic(self):
        assert Mutibs.from_u(42, 8) == Tibs('0b00101010')

    def test_from_i_positive(self):
        assert Mutibs.from_i(42, 8) == Tibs('0b00101010')

    def test_from_f32(self):
        assert len(Mutibs.from_f(3.14, 32)) == 32

    def test_from_bytes(self):
        assert Mutibs.from_bytes(b"abc") == Tibs('0x616263')

    def test_from_string_bin(self):
        assert Mutibs.from_string("0b101") == Tibs('0b101')

    def test_from_bools(self):
        assert Mutibs.from_bools([True, False, 1, 0]) == Tibs('0b1010')

    def test_from_zeros(self):
        assert Mutibs.from_zeros(5) == Tibs('0b00000')

    def test_from_ones(self):
        assert Mutibs.from_ones(5) == Tibs('0b11111')

    def test_from_random(self):
        assert len(Mutibs.from_random(100)) == 100

    def test_from_joined(self):
        assert Mutibs.from_joined(["0b11", "0o7", "0xf"]) == Tibs('0b111111111')

    def test_constructor_delegation(self):
        assert Mutibs("0b101") == Mutibs.from_string("0b101")
        assert Mutibs(b"abc") == Mutibs.from_bytes(b"abc")
        assert Mutibs([True, False]) == Mutibs.from_bools([True, False])

    def test_from_slice(self):
        a = Tibs("0b101100111000")
        b = a[2:10]
        assert b == Tibs('0b11001110')
        c = Mutibs(b)
        assert c == Tibs('0b11001110')


class TestTibsMethods:
    def test_eq(self):
        assert Tibs("0xf") == Tibs('0b1111')
        assert not (Tibs("0xf") == Tibs('0b1110'))

    def test_str(self):
        assert str(Tibs("0b101")) == "0b101"

    def test_repr(self):
        assert repr(Tibs("0b101")) == "Tibs('0b101')"


class TestMutibsMethods:
    def test_eq(self):
        assert Mutibs("0xf") == Tibs('0b1111')
        assert not (Mutibs("0xf") == Tibs('0b1110'))

    def test_str(self):
        assert str(Mutibs("0b101")) == "0b101"

    def test_repr(self):
        assert repr(Mutibs("0b101")) == "Mutibs('0b101')"

    def test_set_index(self):
        m = Mutibs("0b000")
        m[1] = 1
        assert m == Tibs('0b010')

    def test_set_slice(self):
        m = Mutibs("0b00000")
        m[1:4] = "0b111"
        assert m == Tibs('0b01110')

    def test_ixor(self):
        m = Mutibs("0b1100")
        m ^= "0b1010"
        assert m == Tibs('0b0110')

    def test_ior(self):
        m = Mutibs("0b1100")
        m |= "0b1010"
        assert m == Tibs('0b1110')

    def test_iand(self):
        m = Mutibs("0b1100")
        m &= "0b1010"
        assert m == Tibs('0b1000')

    def test_set_from_sequence(self):
        m = Mutibs("0b00000")
        m.set([1, 3])
        assert m == Tibs('0b01010')

    def test_set_from_slice(self):
        m = Mutibs("0b00000000")
        m[1:7:2] = [1, 1, 1]
        assert m == Tibs('0b01010100')

    def test_insert_uses_logical_position(self):
        m = Mutibs("0b1100")

        # Build the expected logical bit sequence (the order used by indexing/iteration).
        expected_logical_bits = list(m.to_tibs())
        expected_logical_bits.insert(1, True)

        m.insert(1, "0b1")

        assert list(m.to_tibs()) == expected_logical_bits


class TestIterators:
    def test_bool_iterator(self):
        t = Tibs("0b101")
        assert list(t) == [True, False, True]

    def test_find_all_iterator(self):
        t = Tibs("0b101101")
        assert list(t.find_all_iter("0b101")) == [0, 3]

    def test_chunks_iterator(self):
        t = Tibs("0b101101")
        chunks = list(t.chunks_iter(3))
        assert len(chunks) == 2
        assert chunks[0] == Tibs('0b101')
        assert chunks[1] == Tibs('0b101')


class TestErrorHandling:

    def test_from_i_errors(self):
        with pytest.raises(OverflowError):
            Tibs.from_i(128, 8)  # Overflows (signed)
        with pytest.raises(OverflowError):
            Tibs.from_i(-129, 8)  # Overflows (signed)

    def test_from_f_errors(self):
        with pytest.raises(ValueError):
            Tibs.from_f(1.0, 12)  # Invalid length

    def test_from_bytes_errors(self):
        with pytest.raises(ValueError):
            Tibs.from_bytes(b"abc", offset=30)
        with pytest.raises(ValueError):
            Tibs.from_bytes(b"abc", length=30)

    def test_adding_nonsense(self):
        with pytest.raises(ValueError):
            Mutibs.from_bools([0]) + "hello"

    def test_slice_assignment_errors(self):
        a = Mutibs('0b000')
        with pytest.raises(IndexError):
            a[5] = 1
        with pytest.raises(IndexError):
            del a[5]


class TestAdvancedFeatures:
    def test_rol(self):
        s = Mutibs("0b0001")
        s.rotate_left(1)
        assert s == Tibs('0b0010')

    def test_ror(self):
        s = Mutibs("0b1000")
        s.rotate_right(1)
        assert s == Tibs('0b0100')

    def test_byte_swap(self):
        s = Mutibs("0x0123456789abcdef")
        s.byte_swap()
        assert s == Tibs('0xefcdab8967452301')

    def test_to_f(self):
        f = 3.14159
        t16 = Tibs.from_f(f, 16)
        assert t16.to_f() != f  # Should be different
        t32 = Tibs.from_f(f, 32)
        assert abs(t32.to_f() - f) < 1e-6
        t64 = Tibs.from_f(f, 64)
        assert t64.to_f() == f

    def test_to_u_i(self):
        assert Tibs("0b11111111").to_u() == 255
        assert Tibs("0b11111111").to_i() == -1
        assert Tibs("0b01111111").to_i() == 127

    def test_find_all_byte_aligned(self):
        a = Tibs("0x00ff00ff")
        assert a.find_all(Tibs('0xff'), byte_aligned=True) == [8, 24]


class TestComplexInteractions:
    def test_overlapping_replace(self):
        s = Mutibs("0b101010")
        s.replace("0b10", "0b1010")
        assert s == Tibs('0b101010101010')

    def test_find_all_overlapping(self):
        s = Tibs("0b1010101")
        assert s.find_all(Tibs('0b101')) == [0, 2, 4]

    def test_chunks_with_remainder(self):
        s = Tibs.from_zeros(10)
        chunks = list(s.chunks(3))
        assert len(chunks) == 4
        assert len(chunks[0]) == 3
        assert len(chunks[1]) == 3
        assert len(chunks[2]) == 3
        assert len(chunks[3]) == 1

    def test_set_slice_with_self_overlap(self):
        m = Mutibs("0b11110000")
        m[2:6] = m[0:4]
        assert m == Tibs('0b11111100')

    def test_int_representation_edges(self):
        assert Tibs.from_u(255, 8).to_i() == -1
        assert Tibs.from_i(-128, 8).to_u() == 128
        with pytest.raises(ValueError):
            Tibs("0b" + "1" * 129).to_u()
        with pytest.raises(ValueError):
            Tibs("0b" + "1" * 129).to_i()

    def test_mixed_type_operations(self):
        m = Mutibs("0b1010")
        t = Tibs("0b0101")
        m |= t
        assert m == Tibs('0b1111')

    def test_chained_mutations(self):
        m = Mutibs("0x1234")
        m.rotate_left(4)
        m.invert()
        m.rotate_right(8)
        assert m == Tibs('0xbedc')

    def test_immutable_slice_mutation_check(self):
        t = Tibs("0x12345678")
        s = t[8:24]
        m = s.to_mutibs()
        m.invert()
        assert t == Tibs('0x12345678')
        assert s != m
        assert s == Tibs('0x3456')

    def test_from_joined_mixed_types(self):
        t = Tibs("0b11")
        m = Mutibs("0b00")
        result = Tibs.from_joined([t, m, "0xff", [1, 0, 1]])
        assert result == Tibs('0b110011111111101')

    def test_delete_and_insert(self):
        m = Mutibs("0xabcdef")
        del m[8:16]
        assert m == Tibs('0xabef')
        m.insert(8, "0b11110000")
        assert m == Tibs('0xabf0ef')

    def test_replace_with_empty_string(self):
        s = Mutibs("0b101010")
        s.replace("0b10", "")
        assert s == Tibs('0b')


class TestSliceOperations:
    def test_slice_eq(self):
        a = Tibs("0b101100111000")
        b = a[2:10]
        c = Tibs("0b11001110")
        assert b == c

    def test_slice_logical_or(self):
        a = Tibs("0b00001111")
        b = Tibs("0b10101010")
        s_a = a[0:8]
        s_b = b[0:8]
        assert s_a | s_b == Tibs('0b10101111')

    def test_slice_logical_and(self):
        a = Tibs("0b00001111")
        b = Tibs("0b10101010")
        s_a = a[0:8]
        s_b = b[0:8]
        assert s_a & s_b == Tibs('0b00001010')

    def test_slice_logical_xor(self):
        a = Tibs("0b00001111")
        b = Tibs("0b10101010")
        s_a = a[0:8]
        s_b = b[0:8]
        assert s_a ^ s_b == Tibs('0b10100101')

    def test_slice_logical_or_offset(self):
        a = Tibs("0b100001111")  # 9 bits
        b = Tibs("0b010101010")  # 9 bits
        s_a = a[1:9]  # offset = 1
        s_b = b[1:9]  # offset = 1
        assert s_a | s_b == Tibs('0b10101111')

    def test_slice_logical_and_offset(self):
        a = Tibs("0b100001111")
        b = Tibs("0b010101010")
        s_a = a[1:9]
        s_b = b[1:9]
        assert s_a & s_b == Tibs('0b00001010')

    def test_find_slice(self):
        a = Tibs("0b101100111000101100")
        b = Tibs("0b11001110")
        c = a[2:10]
        assert c == b
        assert a.find(c) == 2

    def test_find_all_slice(self):
        a = Tibs("0b110011100011001110")
        b = Tibs("0b11001110")
        c = a[0:8]
        assert c == b
        assert list(a.find_all(c)) == [0, 10]

    def test_join_slice(self):
        a = Tibs("0b101100111000")
        b = a[2:10]
        c = Tibs.from_joined([b, b])
        assert c == Tibs('0b1100111011001110')

    def test_mutibs_from_slice(self):
        a = Tibs("0b101100111000")
        b = a[2:10]
        m = Mutibs(b)
        m.invert()
        assert m != b
        assert a == Tibs('0b101100111000')

    def test_logical_op_stepped_slice(self):
        a = Tibs("0b00110011")
        b = Tibs("0b01010101")
        c = a[::2]  # 0101
        d = b[::2]  # 0000
        assert c | d == Tibs('0b0101')
        assert c & d == Tibs('0b0000')
        assert c ^ d == Tibs('0b0101')

    def test_mixed_tibs_mutibs_slice_ops(self):
        a = Tibs("0b11110000")
        b = Mutibs("0b10101010")
        s_a = a[0:8]
        s_b = b[0:8]
        assert s_a & s_b == Tibs('0b10100000')

    def test_mutibs_slice_is_immutable_copy(self):
        a = Mutibs("0b00000000")
        b = a[2:6]
        b[1] = 1
        # The original is unchanged
        assert a == Tibs('0b00000000')

    def test_slice_logical_xor_offset(self):
        a = Tibs("0b100001111")
        b = Tibs("0b010101010")
        s_a = a[1:9]
        s_b = b[1:9]
        assert s_a ^ s_b == Tibs('0b10100101')


class TestKnownRegressions:
    def test_rotate_empty_selected_slice_is_noop(self):
        m = Mutibs("0b101010")
        m.rotate_left(3, start=2, end=2)
        m.rotate_right(1, start=4, end=4)
        assert m == Tibs('0b101010')

    def test_set_negative_step_range(self):
        m = Mutibs("0b00000000")
        m.set(range(7, 3, -1))
        assert m.to_bin() == "00001111"

    def test_unset_negative_step_range(self):
        m = Mutibs("0b11111111")
        m.unset(range(7, 3, -1))
        assert m.to_bin() == "11110000"


class TestDocsMismatchRegressions:
    def test_find_all_empty_needle_raises_value_error_docs_contract(self):
        t = Tibs("0b101")
        with pytest.raises(ValueError):
            print(list(t.find_all("")))

    def test_tibs_from_random_positional_seed_is_deterministic_docs_example(self):
        a = Tibs.from_random(256, seed=b"a_seed")
        b = Tibs.from_random(256, seed=b"a_seed")
        assert a == b

    def test_mutibs_from_random_positional_seed_is_deterministic_docs_example(self):
        a = Mutibs.from_random(256, seed=b"a_seed")
        b = Mutibs.from_random(256, seed=b"a_seed")
        assert a == b

    def test_mutibs_insert_out_of_range_raises_value_error_docs_contract(self):
        m = Mutibs("0b101")
        m.insert(len(m) + 1, "0b1")
        assert m == Tibs('0b1011')

    def test_tibs_to_u_empty_raises_value_error_docs_contract(self):
        with pytest.raises(ValueError):
            Tibs().to_u()

    def test_tibs_to_i_empty_raises_value_error_docs_contract(self):
        with pytest.raises(ValueError):
            Tibs().to_i()


class TestKnownLogicFaults:
    def test_set_range_descending_to_minus_one_includes_zero(self):
        m = Mutibs.from_zeros(4)
        m.set(range(3, -1, -1))
        assert m.to_bin() == "1111"

    def test_unset_range_descending_to_minus_one_includes_zero(self):
        m = Mutibs.from_ones(4)
        m.unset(range(3, -1, -1))
        assert m.to_bin() == "0000"

def _delete_expected(bits: list[bool], key) -> list[bool]:
    expected = bits.copy()
    del expected[key]
    return expected


@pytest.mark.parametrize(
    "key",
    [
        slice(None, 8),
        slice(8, None),
        slice(4, 20),
        slice(-16, -4),
        slice(10, 10),
        slice(None, None, 2),
        slice(1, None, 3),
        slice(None, None, -1),
        slice(20, 2, -3),
    ],
)
def test_del_slice_matches_python_list_semantics(key):
    # Use a non-trivial pattern so direction/indexing errors are obvious.
    m = Mutibs.from_hex("abcdef")
    before = list(m.to_tibs())

    expected = _delete_expected(before, key)
    del m[key]

    assert list(m.to_tibs()) == expected


def _set_expected(bits: list[bool], key, value: list[bool]) -> list[bool]:
    expected = bits.copy()
    expected[key] = value
    return expected


@pytest.mark.parametrize(
    "key,value",
    [
        (slice(None, 8), [True, False, True, False]),
        (slice(8, None), [False, True, False]),
        (slice(4, 20), [True] * 5),
        (slice(-16, -4), [False, False, True, True]),
        (slice(10, 10), [True, False, True]),
    ],
)
def test_set_slice_matches_python_list_semantics(key, value):
    m = Mutibs.from_hex("abcdef")
    before = list(m.to_tibs())
    replacement = Tibs.from_bools(value)

    expected = _set_expected(before, key, list(replacement))
    m[key] = replacement

    assert list(m.to_tibs()) == expected


@pytest.mark.parametrize("key", [slice(None, None, 2), slice(1, None, 3), slice(20, 2, -3)])
def test_set_extended_slice_matches_python_list_semantics(key):
    m = Mutibs.from_hex("abcdef")
    before = list(m.to_tibs())

    target_len = len(before[key])
    value = [(i % 2) == 0 for i in range(target_len)]
    replacement = Tibs.from_bools(value)

    expected = _set_expected(before, key, list(replacement))
    m[key] = replacement

    assert list(m.to_tibs()) == expected


def test_set_extended_slice_length_mismatch_raises():
    m = Mutibs.from_hex("abcdef")
    with pytest.raises(ValueError):
        m[::2] = [1, 0]


def test_del_full_and_single_index_regression():
    m = Mutibs.from_hex("1234")
    before = list(m.to_tibs())

    m2 = Mutibs.from_hex("1234")
    del m2[:]
    assert list(m2.to_tibs()) == []

    del m[0]
    expected = before.copy()
    del expected[0]
    assert list(m.to_tibs()) == expected



def _find_expected(
        bits: list[bool],
        needle: list[bool],
        start: int | None,
        end: int | None,
        reverse: bool,
        byte_aligned: bool,
) -> int | None:
    n = len(bits)
    s = 0 if start is None else start
    e = n if end is None else end
    if s < 0:
        s += n
    if e < 0:
        e += n
    m = len(needle)
    if m == 0 or m > (e - s):
        return None

    if reverse:
        i_range = range(e - m, s - 1, -1)
    else:
        i_range = range(s, e - m + 1)

    for i in i_range:
        if byte_aligned and (i % 8 != 0):
            continue
        if bits[i:i + m] == needle:
            return i
    return None


@pytest.mark.parametrize(
    "needle,start,end,byte_aligned",
    [
        ([1, 1], None, None, False),
        ([1, 0, 1], 3, 28, False),
        ([0, 1], -20, -1, False),
        ([1, 1, 0], 0, 32, True),
        ([0, 0, 1], 5, 31, True),
    ],
)
def test_find_rfind_match_python_reference(needle, start, end, byte_aligned):
    t = Tibs("0xd35a1c9e")
    bits = list(t)
    needle_bits = [bool(x) for x in needle]

    expected_find = _find_expected(bits, needle_bits, start, end, False, byte_aligned)
    expected_rfind = _find_expected(bits, needle_bits, start, end, True, byte_aligned)

    assert t.find(needle, start=start, end=end, byte_aligned=byte_aligned) == expected_find
    assert t.rfind(needle, start=start, end=end, byte_aligned=byte_aligned) == expected_rfind


def _rotate_left_expected(bits: list[bool], start: int, end: int, n: int) -> list[bool]:
    expected = bits.copy()
    segment = expected[start:end]
    if not segment:
        return expected
    n %= len(segment)
    expected[start:end] = segment[n:] + segment[:n]
    return expected


class TestConcreteRegressionCases:
    def test_find_byte_aligned_empty_subrange_returns_none(self):
        t = Tibs("0x11223344")
        assert t.find("0x11", start=1, end=1, byte_aligned=True) is None

    def test_find_all_byte_aligned_empty_subrange_returns_empty_list(self):
        t = Tibs("0x11223344")
        assert t.find_all(Tibs('0x11'), start=1, end=1, byte_aligned=True) == []

    def test_find_all_single_bit_respects_slice_and_alignment(self):
        t = Tibs("0b1000000010000000100000001")

        assert t.find_all("0b1") == [0, 8, 16, 24]
        assert t.find_all("0b1", start=1, end=24, byte_aligned=True) == [8, 16]

        zeros = Tibs("0b011111110111111101111111")
        assert zeros.find_all("0b0", byte_aligned=True) == [0, 8, 16]

    def test_find_all_single_pass_preserves_overlapping_matches(self):
        t = Tibs("0b11111")
        assert t.find_all("0b111") == [0, 1, 2]

    def test_find_all_byte_aligned_respects_subrange(self):
        t = Tibs("0xabcdabcd")
        assert t.find_all("0xabcd", start=8, end=32, byte_aligned=True) == [16]

    def test_mutibs_find_all_uses_same_search_semantics(self):
        m = Mutibs("0b11111")
        assert m.find_all("0b111") == [0, 1, 2]
        assert m.find_all("0b1", byte_aligned=True) == [0]

    def test_rfind_reverse_search_finds_prefix_match(self):
        t = Tibs("0b0111")
        assert t.rfind("0b011") == 0

    def test_mutibs_rotate_left_partial_slice_matches_logical_rotation(self):
        m = Mutibs("0b10110010")
        expected = _rotate_left_expected(list(m.to_tibs()), 1, 7, 2)

        m.rotate_left(2, start=1, end=7)

        assert list(m.to_tibs()) == expected

    def test_tibs_rotated_left_partial_slice_matches_logical_rotation(self):
        t = Tibs("0b10110010")
        expected = _rotate_left_expected(list(t), 1, 7, 2)

        rotated = t.rotated_left(2, start=1, end=7)

        assert list(rotated) == expected


def test_decode_malformed_zstd_with_impossible_padding_raises_value_error():
    malformed = bytes.fromhex("110d28b52ffd240001000099e9d851")
    with pytest.raises(ValueError):
        Mutibs.decode(malformed)



def test_decode_malformed_zstd_with_impossible_padding_raises_value_error():
    # Valid zstd frame for empty bytes, wrapped in the tibs zstd container
    # with an impossible bit_padding of 1 for zero decompressed bits.
    malformed = bytes.fromhex("110d28b52ffd240001000099e9d851")
    with pytest.raises(ValueError):
        Tibs.decode(malformed)


def test_bit_ops_unequal_offsets():
    a = Tibs('0b11001010_01110100_10111100')
    b = Tibs('0b10110101_11001001_00101110')
    left = a[3:21]
    right = b[5:23]

    expected_or = [x or y for x, y in zip(left, right)]
    expected_and = [x and y for x, y in zip(left, right)]
    expected_xor = [x != y for x, y in zip(left, right)]

    assert list(left | right) == expected_or
    assert list(left & right) == expected_and
    assert list(left ^ right) == expected_xor


class TestAdditionalCoverage:
    def test_tibs_set_unset_and_inverted_return_new_instances(self):
        t = Tibs("0b10110010")

        assert t.set_at(range(1, 8, 2)) == Tibs("0b11110111")
        assert Tibs("0b11111111").unset_at([-1, -3, 0]) == Tibs("0b01111010")
        assert Tibs("0b10000001").inverted(range(1, 7)) == Tibs("0b11111111")
        assert t == Tibs("0b10110010")

    def test_reverse_iterators_have_obvious_order_and_limits(self):
        t = Tibs("0b1010110010")

        assert list(t.rfind_all_iter("0b10")) == [8, 5, 2, 0]
        assert [chunk.bin for chunk in t.rchunks_iter(3)] == ["010", "110", "010", "1"]
        assert [chunk.bin for chunk in t.rchunks_iter(3, count=2)] == ["010", "110"]

    def test_tibs_byte_swapped_slice_returns_new_instance(self):
        t = Tibs("0x001122334455")

        assert t.byte_swapped(start=8, end=40) == Tibs("0x004433221155")
        assert t.byte_swapped(2, start=8, end=40) == Tibs("0x002211443355")
        assert t == Tibs("0x001122334455")

    def test_to_raw_data_reports_offset_and_length_for_unaligned_slice(self):
        t = Tibs("0x123456")[4:20]

        raw_bytes, offset, length = t.to_raw_data()

        assert raw_bytes == b"\x12\x34\x56"
        assert offset == 4
        assert length == 16
        assert t == Tibs("0x2345")

    def test_mutibs_append_pop_and_empty_pop_error(self):
        m = Mutibs("0b10")

        assert m.append(1) is None
        assert m == Tibs("0b101")
        assert m.pop() is True
        assert m.pop() is False
        assert m == Tibs("0b1")

        empty = Mutibs()
        with pytest.raises(IndexError):
            empty.pop()

    def test_mutibs_as_tibs_moves_data_out_and_empties_source(self):
        m = Mutibs("0x1234")

        moved = m.as_tibs()

        assert moved == Tibs("0x1234")
        assert m == Tibs()
        assert len(m) == 0

    def test_mutibs_as_raw_data_moves_data_out_and_empties_source(self):
        m = Mutibs("0x1234")

        assert m.as_raw_data() == (b"\x12\x34", 0, 16)
        assert m == Tibs()
        assert len(m) == 0

    def test_mutable_view_field_write_updates_underlying_source(self):
        m = Mutibs("0x0000")
        field = m.lsb0.le.field(15, 8)

        assert field.u == 0
        field.u = 0xab

        assert field.u == 0xab
        assert m == Mutibs("0x00ab")

    def test_shift_operators_zero_fill_and_keep_length(self):
        t = Tibs("0b1011")

        assert t << 2 == Tibs("0b1100")
        assert t >> 2 == Tibs("0b0010")
        assert t == Tibs("0b1011")

        m = Mutibs("0b1011")
        m <<= 2
        assert m == Tibs("0b1100")
        m >>= 2
        assert m == Tibs("0b0011")

    def test_reflected_add_and_multiply_accept_promotable_values(self):
        assert "0b11" + Tibs("0b00") == Tibs("0b1100")
        assert 3 * Tibs("0b10") == Tibs("0b101010")
        assert Mutibs("0b01") * 3 == Tibs("0b010101")

    def test_mutibs_decode_malformed_zstd_with_impossible_padding_raises_value_error(self):
        malformed = bytes.fromhex("110d28b52ffd240001000099e9d851")
        with pytest.raises(ValueError):
            Mutibs.decode(malformed)

    def test_bytes_apis_reject_integer_sequences(self):
        for cls in (Tibs, Mutibs):
            for value in ([0x61, 0x62], (0x61, 0x62)):
                with pytest.raises(TypeError):
                    cls.from_bytes(value)

        m = Mutibs("0x0000")
        for value in ([0x61, 0x62], (0x61, 0x62)):
            with pytest.raises(TypeError):
                m.write_bytes(value)
            with pytest.raises(TypeError):
                m.view().write_bytes(value)


class TestDtypeViewMutableViewEdgeCoverage:
    def test_dtype_spec_is_normalized_and_usable(self):
        d = Dtype("  U16_LE  ")

        assert repr(d) == "Dtype('u16_le')"
        assert d.kind is DtypeKind.Uint
        assert d.length == 16
        assert d.byte_order is ByteOrder.Little
        assert d.pack(0x1234) == Tibs("0x3412")
        assert d.unpack("0x3412") == 0x1234

    def test_dtype_from_params_accepts_none_byte_order_and_properties_are_read_only(self):
        d = Dtype.from_params(DtypeKind.Int, 8, None)

        assert repr(d) == "Dtype('i8')"
        assert d.byte_order is ByteOrder.Unspecified

        for name in ("kind", "length", "byte_order"):
            with pytest.raises(AttributeError):
                setattr(d, name, None)

    def test_dtype_unpack_methods_accept_negative_ranges_and_bytes_like_inputs(self):
        d = Dtype("u8")
        bits = Tibs.from_bytes(b"\x01\x02\x03\x04")

        assert d.unpack(bits, -24, -16) == 2
        assert d.unpack_values(bits, -24, -8) == [2, 3]
        assert list(d.unpack_values_iter(bits, -32, -8)) == [1, 2, 3]
        assert d.unpack_values(bytearray(b"\x05\x06")) == [5, 6]
        assert d.unpack_values(memoryview(b"\x07\x08")) == [7, 8]

    def test_dtype_pack_values_reports_late_item_overflow(self):
        with pytest.raises(OverflowError):
            Dtype("u4").pack_values([1, 2, 16])

    def test_dtype_bytes_pack_rejects_overlong_values(self):
        for value in (b"abc", bytearray(b"abc"), memoryview(b"abc")):
            with pytest.raises(ValueError):
                Dtype("bytes16").pack(value)
            with pytest.raises(ValueError):
                Tibs.from_value("bytes16", value)
            with pytest.raises(ValueError):
                Mutibs.from_value("bytes16", value)

    def test_dtype_bytes_pack_values_rejects_overlong_items(self):
        value_sets = [
            [b"ab", b"abc"],
            [bytearray(b"ab"), bytearray(b"abc")],
            [memoryview(b"ab"), memoryview(b"abc")],
        ]

        for values in value_sets:
            with pytest.raises(ValueError):
                Dtype("bytes16").pack_values(values)
            with pytest.raises(ValueError):
                Tibs.from_values("bytes16", values)
            with pytest.raises(ValueError):
                Mutibs.from_values("bytes16", values)

    def test_dtype_bytes_pack_rejects_integer_sequences(self):
        for value in ([0x61, 0x62], (0x61, 0x62)):
            with pytest.raises(TypeError):
                Dtype("bytes16").pack(value)
            with pytest.raises(TypeError):
                Tibs.from_value("bytes16", value)
            with pytest.raises(TypeError):
                Mutibs.from_value("bytes16", value)

    def test_dtype_float_specs_reject_unsupported_widths_at_construction(self):
        for length in (8, 24, 128):
            with pytest.raises(ValueError):
                Dtype(f"f{length}")
            with pytest.raises(ValueError):
                Dtype.from_params(DtypeKind.Float, length)

    def test_dtype_bytes_specs_reject_non_byte_widths_at_construction(self):
        for length in (1, 7, 9):
            with pytest.raises(ValueError):
                Dtype(f"bytes{length}")
            with pytest.raises(ValueError):
                Dtype.from_params(DtypeKind.Bytes, length)

    def test_dtype_text_specs_reject_non_digit_aligned_widths_at_construction(self):
        cases = [
            ("hex", DtypeKind.Hex, 7),
            ("oct", DtypeKind.Oct, 8),
        ]

        for prefix, kind, length in cases:
            with pytest.raises(ValueError):
                Dtype(f"{prefix}{length}")
            with pytest.raises(ValueError):
                Dtype.from_params(kind, length)

    def test_view_from_indices_empty_selection_is_empty_and_unusable_as_number(self):
        v = View.from_indices(Tibs("0xff"), [])

        assert len(v) == 0
        assert v.bin == ""
        assert v.to_tibs() == Tibs()
        assert v.to_mutibs() == Mutibs()

        with pytest.raises(ValueError):
            _ = v.u
        with pytest.raises(ValueError):
            v.field(0, 0)

    def test_view_from_indices_respects_layout_for_two_byte_selection(self):
        source = Tibs("0x1234")

        lsb0 = View.from_indices(source, range(16), bit_order=BitOrder.Lsb0)
        little = View.from_indices(source, range(16), byte_order=ByteOrder.Little)

        assert lsb0.hex == "3412"
        assert little.hex == "3412"
        assert lsb0.to_tibs() == little.to_tibs() == Tibs("0x3412")

    def test_view_non_byte_field_rejects_byte_oriented_reinterpretation(self):
        field = Tibs("0xff").field(0, 3)

        assert field.bin == "1111"
        with pytest.raises(ValueError):
            _ = field.le
        with pytest.raises(ValueError):
            _ = field.lsb0

    def test_mutable_view_empty_selection_allows_empty_write_only(self):
        m = Mutibs("0xff")
        v = MutableView.from_indices(m, [])

        assert len(v) == 0
        assert v.bin == ""
        assert v.write_bin("") is None
        assert m == Mutibs("0xff")

        with pytest.raises(ValueError):
            v.write_bin("1")

    def test_mutable_view_sparse_byte_write_updates_selected_source_bits(self):
        m = Mutibs("0x0000")
        v = MutableView.from_indices(m, [0, 2, 4, 6, 8, 10, 12, 14])

        assert v.bytes == b"\x00"
        v.bytes = b"\xff"

        assert v.bytes == b"\xff"
        assert m == Mutibs("0xaaaa")

    def test_mutable_view_revalidates_selected_indices_after_source_shrinks(self):
        m = Mutibs("0xff")
        v = MutableView.from_indices(m, [6, 7])

        assert v.bin == "11"
        del m[6:]

        with pytest.raises(ValueError):
            len(v)
        with pytest.raises(ValueError):
            _ = v.bin

    def test_mutable_view_signed_write_failure_leaves_source_unchanged(self):
        m = Mutibs.from_zeros(8)

        m.view().i = -1
        assert m == Mutibs("0xff")

        original = m.to_tibs()
        with pytest.raises(OverflowError):
            m.view().i = -129

        assert m == original

    def test_mutable_view_to_mutibs_materializes_copy(self):
        m = Mutibs("0x1234")

        copy = m.lsb0.to_mutibs()
        copy[0] = True

        assert copy == Mutibs("0xb412")
        assert m == Mutibs("0x1234")


# ---------------------------------------------------------------------------
# Property-based fuzz tests (hypothesis).
#
# Every generated Tibs/Mutibs is (optionally) built as a slice of a larger
# parent object, so the underlying storage often starts mid-byte. Operations
# are checked against a pure-Python list-of-bools / binary-string model.
# ---------------------------------------------------------------------------

from hypothesis import assume, given, settings
import hypothesis.strategies as st

FUZZ = settings(max_examples=75, deadline=None)


def _ref_bin(bits):
    return "".join("1" if b else "0" for b in bits)


def _normalize_strict_bounds(length, start, end):
    start = 0 if start is None else start
    end = length if end is None else end
    if start < 0:
        start += length
    if end < 0:
        end += length
    if not (0 <= start <= end <= length):
        raise ValueError
    return start, end


@st.composite
def bool_lists(draw, min_size=0, max_size=200):
    # Mix densities so low-entropy data stresses fallback search paths.
    density = draw(st.sampled_from([2, 50, 98]))
    n = draw(st.integers(min_size, max_size))
    return [draw(st.integers(0, 99)) < density for _ in range(n)]


@st.composite
def padded_tibs(draw, min_size=0, max_size=200):
    """A Tibs plus its reference bits, often stored at a bit offset."""
    bits = draw(bool_lists(min_size=min_size, max_size=max_size))
    pre = draw(bool_lists(max_size=13))
    post = draw(bool_lists(max_size=13))
    t = Tibs.from_bools(pre + bits + post)[len(pre):len(pre) + len(bits)]
    return t, bits


@st.composite
def padded_mutibs(draw, min_size=0, max_size=200):
    """A Mutibs plus its reference bits, often stored at a bit offset."""
    bits = draw(bool_lists(min_size=min_size, max_size=max_size))
    pre = draw(bool_lists(max_size=13))
    post = draw(bool_lists(max_size=13))
    m = Mutibs.from_bools(pre + bits + post)[len(pre):len(pre) + len(bits)]
    return m, bits


def _ref_occurrences(s, p):
    return [i for i in range(len(s) - len(p) + 1) if s.startswith(p, i)]


class TestFuzzSearch:
    @FUZZ
    @given(padded_tibs(min_size=1), st.data())
    def test_find_family_matches_string_reference(self, tb, data):
        t, bits = tb
        s = _ref_bin(bits)
        # Needle: usually a real substring (so matches exist), sometimes random.
        # Lengths deliberately straddle the 64-bit small/large search boundary.
        nlen = data.draw(st.integers(1, min(len(bits), 90)), label="needle_len")
        if data.draw(st.booleans(), label="from_haystack"):
            at = data.draw(st.integers(0, len(bits) - nlen), label="at")
            nbits = bits[at:at + nlen]
        else:
            nbits = data.draw(bool_lists(min_size=nlen, max_size=nlen))
        needle = Tibs.from_bools(nbits)
        p = _ref_bin(nbits)

        occurrences = _ref_occurrences(s, p)
        expected_find = occurrences[0] if occurrences else None
        expected_rfind = occurrences[-1] if occurrences else None

        assert t.find(needle) == expected_find
        assert t.rfind(needle) == expected_rfind
        assert t.find_all(needle) == occurrences
        assert list(t.find_all_iter(needle)) == occurrences
        assert list(t.rfind_all_iter(needle)) == occurrences[::-1]
        assert t.count(needle) == len(occurrences)

    @FUZZ
    @given(padded_tibs(min_size=1), st.data())
    def test_find_with_bounds_and_byte_alignment(self, tb, data):
        t, bits = tb
        s = _ref_bin(bits)
        nlen = data.draw(st.integers(1, min(len(bits), 70)), label="needle_len")
        at = data.draw(st.integers(0, len(bits) - nlen), label="at")
        nbits = bits[at:at + nlen]
        needle = Tibs.from_bools(nbits)
        p = _ref_bin(nbits)
        start = data.draw(st.integers(0, len(bits)), label="start")
        end = data.draw(st.integers(start, len(bits)), label="end")
        aligned = data.draw(st.booleans(), label="aligned")

        occurrences = [
            i for i in _ref_occurrences(s, p)
            if i >= start and i + nlen <= end and (not aligned or i % 8 == 0)
        ]
        assert t.find(needle, start, end, byte_aligned=aligned) == (
            occurrences[0] if occurrences else None
        )
        assert t.rfind(needle, start, end, byte_aligned=aligned) == (
            occurrences[-1] if occurrences else None
        )
        assert t.find_all(needle, start, end, byte_aligned=aligned) == occurrences

    @FUZZ
    @given(padded_tibs(min_size=1), st.data())
    def test_find_with_negative_bounds_matches_strict_reference(self, tb, data):
        t, bits = tb
        s = _ref_bin(bits)
        nlen = data.draw(st.integers(1, min(len(bits), 90)), label="needle_len")
        if data.draw(st.booleans(), label="from_haystack"):
            at = data.draw(st.integers(0, len(bits) - nlen), label="at")
            nbits = bits[at:at + nlen]
        else:
            nbits = data.draw(bool_lists(min_size=nlen, max_size=nlen), label="needle")
        needle = Tibs.from_bools(nbits)
        p = _ref_bin(nbits)
        start = data.draw(
            st.one_of(st.none(), st.integers(-len(bits) - 3, len(bits) + 3)),
            label="start",
        )
        end = data.draw(
            st.one_of(st.none(), st.integers(-len(bits) - 3, len(bits) + 3)),
            label="end",
        )
        aligned = data.draw(st.booleans(), label="byte_aligned")

        try:
            start_norm, end_norm = _normalize_strict_bounds(len(bits), start, end)
        except ValueError:
            with pytest.raises(ValueError):
                t.find(needle, start, end, byte_aligned=aligned)
            with pytest.raises(ValueError):
                t.rfind(needle, start, end, byte_aligned=aligned)
            with pytest.raises(ValueError):
                t.find_all(needle, start, end, byte_aligned=aligned)
            with pytest.raises(ValueError):
                list(t.find_all_iter(needle, start, end, byte_aligned=aligned))
            with pytest.raises(ValueError):
                list(t.rfind_all_iter(needle, start, end, byte_aligned=aligned))
            return

        occurrences = [
            i for i in range(start_norm, end_norm - nlen + 1)
            if (not aligned or i % 8 == 0) and s.startswith(p, i)
        ]
        assert t.find(needle, start, end, byte_aligned=aligned) == (
            occurrences[0] if occurrences else None
        )
        assert t.rfind(needle, start, end, byte_aligned=aligned) == (
            occurrences[-1] if occurrences else None
        )
        assert t.find_all(needle, start, end, byte_aligned=aligned) == occurrences
        assert list(t.find_all_iter(needle, start, end, byte_aligned=aligned)) == occurrences
        assert list(t.rfind_all_iter(needle, start, end, byte_aligned=aligned)) == (
            occurrences[::-1]
        )

    @FUZZ
    @given(padded_tibs(), st.data())
    def test_count_single_bit_with_bounds(self, tb, data):
        t, bits = tb
        start = data.draw(st.integers(0, len(bits)), label="start")
        end = data.draw(st.integers(start, len(bits)), label="end")
        assert t.count(1, start, end) == sum(bits[start:end])
        assert t.count(0, start, end) == (end - start) - sum(bits[start:end])

    @FUZZ
    @given(padded_tibs(), st.data())
    def test_count_single_bit_with_negative_bounds_matches_reference(self, tb, data):
        t, bits = tb
        start = data.draw(
            st.one_of(st.none(), st.integers(-len(bits) - 3, len(bits) + 3)),
            label="start",
        )
        end = data.draw(
            st.one_of(st.none(), st.integers(-len(bits) - 3, len(bits) + 3)),
            label="end",
        )

        try:
            start_norm, end_norm = _normalize_strict_bounds(len(bits), start, end)
        except ValueError:
            with pytest.raises(ValueError):
                t.count(1, start, end)
            with pytest.raises(ValueError):
                t.count(0, start, end)
            return

        selected = bits[start_norm:end_norm]
        assert t.count(1, start, end) == sum(selected)
        assert t.count(0, start, end) == len(selected) - sum(selected)

    @FUZZ
    @given(padded_tibs(min_size=1), st.data())
    def test_replaced_matches_string_reference(self, tb, data):
        t, bits = tb
        s = _ref_bin(bits)
        nlen = data.draw(st.integers(1, min(len(bits), 20)), label="old_len")
        at = data.draw(st.integers(0, len(bits) - nlen), label="at")
        old = bits[at:at + nlen]
        new = data.draw(bool_lists(max_size=12), label="new")

        # Left-to-right, non-overlapping replacement.
        out, i = [], 0
        while True:
            j = s.find(_ref_bin(old), i)
            if j == -1:
                out.append(s[i:])
                break
            out.append(s[i:j])
            out.append(_ref_bin(new))
            i = j + nlen
        expected = "".join(out)

        replaced = t.replaced(Tibs.from_bools(old), Tibs.from_bools(new))
        assert replaced.to_bin() == expected


class TestFuzzSlicingAndViews:
    @FUZZ
    @given(padded_tibs(), st.data())
    def test_step_slicing_matches_list(self, tb, data):
        t, bits = tb
        step = data.draw(st.sampled_from([-7, -3, -2, -1, 1, 2, 3, 7]), label="step")
        start = data.draw(st.one_of(st.none(), st.integers(-len(bits) - 2, len(bits) + 2)), label="start")
        stop = data.draw(st.one_of(st.none(), st.integers(-len(bits) - 2, len(bits) + 2)), label="stop")
        assert t[start:stop:step].to_bin() == _ref_bin(bits[start:stop:step])

    @FUZZ
    @given(padded_tibs())
    def test_to_bools_and_iteration_match(self, tb):
        t, bits = tb
        assert t.to_bools() == bits
        assert list(t) == bits
        assert t.to_bin() == _ref_bin(bits)
        assert t.to_mutibs().to_bools() == bits

    @FUZZ
    @given(padded_tibs(max_size=256))
    def test_to_raw_data_reconstructs_offset_slices(self, tb):
        t, bits = tb

        raw_bytes, offset, length = t.to_raw_data()

        assert length == len(bits)
        assert Tibs.from_bytes(raw_bytes, offset, length).to_bools() == bits

    @FUZZ
    @given(padded_mutibs(max_size=256))
    def test_mutibs_as_raw_data_reconstructs_offset_slices_and_empties_source(self, mb):
        m, bits = mb

        raw_bytes, offset, length = m.as_raw_data()

        assert length == len(bits)
        assert Tibs.from_bytes(raw_bytes, offset, length).to_bools() == bits
        assert m == Mutibs()

    @FUZZ
    @given(padded_tibs())
    def test_reversed_matches_list(self, tb):
        t, bits = tb
        assert t.reversed().to_bin() == _ref_bin(bits[::-1])

    @FUZZ
    @given(padded_tibs(min_size=1), st.data())
    def test_rotations_match_reference(self, tb, data):
        t, bits = tb
        n = data.draw(st.integers(0, 2 * len(bits) + 8), label="n")
        start = data.draw(st.integers(0, len(bits)), label="start")
        end = data.draw(st.integers(start, len(bits)), label="end")

        seg = bits[start:end]
        if seg:
            k = n % len(seg)
            left = bits[:start] + seg[k:] + seg[:k] + bits[end:]
            right = bits[:start] + (seg[-k:] + seg[:-k] if k else seg) + bits[end:]
        else:
            left = right = bits[:]
        assert t.rotated_left(n, start, end).to_bin() == _ref_bin(left)
        assert t.rotated_right(n, start, end).to_bin() == _ref_bin(right)

    @FUZZ
    @given(padded_tibs(min_size=1), st.data())
    def test_shifts_match_reference(self, tb, data):
        t, bits = tb
        n = data.draw(st.integers(0, len(bits) + 4), label="n")
        shifted_left = bits[n:] + [False] * min(n, len(bits))
        shifted_right = [False] * min(n, len(bits)) + bits[:max(len(bits) - n, 0)]
        assert (t << n).to_bin() == _ref_bin(shifted_left)
        assert (t >> n).to_bin() == _ref_bin(shifted_right)

    @FUZZ
    @given(padded_tibs(), st.data())
    def test_split_at_and_chunks_match_reference(self, tb, data):
        t, bits = tb
        positions = sorted(data.draw(st.lists(st.integers(0, len(bits)), max_size=6), label="positions"))
        parts = t.split_at(positions)
        bounds = [0] + positions + [len(bits)]
        assert [p.to_bin() for p in parts] == [
            _ref_bin(bits[a:b]) for a, b in zip(bounds, bounds[1:])
        ]

        chunk_size = data.draw(st.integers(1, len(bits) + 3), label="chunk_size")
        assert [c.to_bin() for c in t.chunks(chunk_size)] == [
            _ref_bin(bits[i:i + chunk_size]) for i in range(0, len(bits), chunk_size)
        ]

    @FUZZ
    @given(st.lists(padded_tibs(max_size=40), max_size=8))
    def test_from_joined_matches_concatenation(self, items):
        joined = Tibs.from_joined([t for t, _ in items])
        assert joined.to_bin() == "".join(_ref_bin(bits) for _, bits in items)

    @FUZZ
    @given(padded_tibs(max_size=80), st.data())
    def test_view_from_indices_matches_selected_bits(self, tb, data):
        t, bits = tb
        if bits:
            indices = data.draw(
                st.lists(st.integers(0, len(bits) - 1), unique=True, max_size=25),
                label="indices",
            )
        else:
            indices = []

        view = View.from_indices(t, indices)

        assert view.bin == _ref_bin([bits[i] for i in indices])
        assert view.to_tibs().to_bools() == [bits[i] for i in indices]


class TestFuzzLogicalAndNumeric:
    @FUZZ
    @given(st.data())
    def test_logical_ops_match_reference(self, data):
        n = data.draw(st.integers(1, 150), label="len")
        a_bits = data.draw(bool_lists(min_size=n, max_size=n), label="a")
        b_bits = data.draw(bool_lists(min_size=n, max_size=n), label="b")
        pre_a = data.draw(bool_lists(max_size=13), label="pre_a")
        pre_b = data.draw(bool_lists(max_size=13), label="pre_b")
        a = Tibs.from_bools(pre_a + a_bits)[len(pre_a):]
        b = Tibs.from_bools(pre_b + b_bits)[len(pre_b):]

        assert (a & b).to_bools() == [x and y for x, y in zip(a_bits, b_bits)]
        assert (a | b).to_bools() == [x or y for x, y in zip(a_bits, b_bits)]
        assert (a ^ b).to_bools() == [x != y for x, y in zip(a_bits, b_bits)]
        assert (~a).to_bools() == [not x for x in a_bits]

    @FUZZ
    @given(bool_lists(max_size=200), bool_lists(max_size=13), bool_lists(max_size=13))
    def test_hash_matches_equality_for_different_offsets(self, bits, pre_a, pre_b):
        a = Tibs.from_bools(pre_a + bits)[len(pre_a):]
        b = Tibs.from_bools(pre_b + bits)[len(pre_b):]

        assert a == b
        assert hash(a) == hash(b)
        assert {a: "value"}[b] == "value"

    @FUZZ
    @given(
        padded_tibs(max_size=512),
        st.sampled_from([Codec.Auto, Codec.Raw, Codec.Rice, Codec.Zstd]),
    )
    def test_encode_decode_roundtrip_preserves_offset_slices(self, tb, codec):
        t, bits = tb

        encoded = t.encode(codec)

        assert Tibs.decode(encoded).to_bools() == bits
        assert Mutibs.decode(encoded).to_bools() == bits

    @FUZZ
    @given(padded_tibs(min_size=1, max_size=120))
    def test_u_and_i_match_reference(self, tb):
        t, bits = tb
        s = _ref_bin(bits)
        u = int(s, 2)
        assert t.to_u() == u
        i = u - (1 << len(bits)) if bits[0] else u
        assert t.to_i() == i
        assert Tibs.from_u(u, len(bits)) == t
        assert Tibs.from_i(i, len(bits)) == t

    @FUZZ
    @given(padded_tibs(min_size=8, max_size=128), st.data())
    def test_byte_swapped_matches_reference(self, tb, data):
        t, bits = tb
        length = (len(bits) // 8) * 8
        t = t[:length]
        bits = bits[:length]
        swapped = []
        for chunk_start in range(length - 8, -8, -8):
            swapped.extend(bits[chunk_start:chunk_start + 8])
        assert t.byte_swapped().to_bin() == _ref_bin(swapped)

    @FUZZ
    @given(st.sampled_from(["u8", "u16", "u16_le", "i8", "i16_le", "u12", "i5"]), st.data())
    def test_pack_unpack_values_roundtrip(self, dtype, data):
        kind = dtype[0]
        length = int(dtype[1:].split("_")[0])
        if kind == "u":
            values = data.draw(st.lists(st.integers(0, (1 << length) - 1), max_size=30), label="values")
        else:
            values = data.draw(
                st.lists(st.integers(-(1 << (length - 1)), (1 << (length - 1)) - 1), max_size=30),
                label="values",
            )
        packed = Tibs.from_values(dtype, values)
        assert len(packed) == length * len(values)
        assert packed.to_values(dtype) == values


class TestFuzzMutation:
    @FUZZ
    @given(padded_mutibs(), st.data())
    def test_setitem_slice_matches_list_model(self, mb, data):
        m, bits = mb
        ref = bits[:]
        start = data.draw(st.integers(0, len(ref)), label="start")
        stop = data.draw(st.integers(start, len(ref)), label="stop")
        # Include byte-multiple lengths to exercise the aligned fast path.
        if data.draw(st.booleans(), label="same_length"):
            vlen = stop - start
        else:
            vlen = data.draw(st.integers(0, 40), label="vlen")
        value = data.draw(bool_lists(min_size=vlen, max_size=vlen), label="value")

        m[start:stop] = Mutibs.from_bools(value) if value else Mutibs()
        ref[start:stop] = value
        assert m.to_bools() == ref

    @FUZZ
    @given(padded_mutibs(max_size=80), st.data())
    def test_extended_slice_length_mismatch_leaves_value_unchanged(self, mb, data):
        m, bits = mb
        original = bits[:]
        start = data.draw(
            st.one_of(st.none(), st.integers(-len(bits) - 3, len(bits) + 3)),
            label="start",
        )
        stop = data.draw(
            st.one_of(st.none(), st.integers(-len(bits) - 3, len(bits) + 3)),
            label="stop",
        )
        step = data.draw(st.sampled_from([-5, -2, -1, 2, 3, 5]), label="step")
        target_len = len(bits[slice(start, stop, step)])
        value = data.draw(
            bool_lists(min_size=target_len + 1, max_size=target_len + 1),
            label="value",
        )

        with pytest.raises(ValueError):
            m[slice(start, stop, step)] = Tibs.from_bools(value)

        assert m.to_bools() == original

    @FUZZ
    @given(padded_mutibs(), st.data())
    def test_delitem_matches_list_model(self, mb, data):
        m, bits = mb
        ref = bits[:]
        start = data.draw(st.integers(0, len(ref)), label="start")
        stop = data.draw(st.integers(start, len(ref)), label="stop")
        step = data.draw(st.sampled_from([None, 1, 2, 3]), label="step")
        if step is None:
            del m[start:stop]
            del ref[start:stop]
        else:
            del m[start:stop:step]
            del ref[start:stop:step]
        assert m.to_bools() == ref

    @FUZZ
    @given(padded_mutibs(min_size=1), st.data())
    def test_set_unset_positions_match_model(self, mb, data):
        m, bits = mb
        ref = bits[:]
        n = len(ref)
        for _ in range(data.draw(st.integers(1, 4), label="rounds")):
            choice = data.draw(st.sampled_from(["int", "list", "range"]), label="kind")
            value = data.draw(st.booleans(), label="value")
            if choice == "int":
                pos = data.draw(st.integers(-n, n - 1), label="pos")
                positions = [pos]
                arg = pos
            elif choice == "list":
                positions = data.draw(st.lists(st.integers(-n, n - 1), max_size=20), label="positions")
                arg = positions
            else:
                # A range must behave exactly like the list of its contents,
                # including negative values and out-of-bounds errors.
                a = data.draw(st.integers(-2 * n - 2, 2 * n + 2), label="a")
                b = data.draw(st.integers(-2 * n - 2, 2 * n + 2), label="b")
                step = data.draw(st.sampled_from([1, 2, 5, -1, -3]), label="step")
                arg = range(a, b, step)
                positions = list(arg)
            method = m.set if value else m.unset
            if any(p < -n or p >= n for p in positions):
                with pytest.raises(IndexError):
                    method(arg)
                continue  # Validation happens up front, so m is unchanged.
            method(arg)
            for p in positions:
                ref[p] = value
        assert m.to_bools() == ref

    @FUZZ
    @given(padded_mutibs(), st.data())
    def test_insert_and_extend_match_model(self, mb, data):
        m, bits = mb
        ref = bits[:]
        extra = data.draw(bool_lists(max_size=30), label="extra")
        action = data.draw(st.sampled_from(["insert", "extend", "extend_left", "append_bool"]), label="action")
        if action == "insert":
            pos = data.draw(st.integers(0, len(ref)), label="pos")
            m.insert(pos, Tibs.from_bools(extra))
            ref[pos:pos] = extra
        elif action == "extend":
            m.extend(extra)
            ref.extend(extra)
        elif action == "extend_left":
            m.extend_left(Tibs.from_bools(extra))
            ref[0:0] = extra
        else:
            m.append(True)
            ref.append(True)
        assert m.to_bools() == ref

    @FUZZ
    @given(padded_mutibs(max_size=100))
    def test_pop_matches_list_model(self, mb):
        m, bits = mb
        ref = bits[:]

        while ref:
            assert m.pop() is ref.pop()
            assert m.to_bools() == ref

        with pytest.raises(IndexError):
            m.pop()

    @FUZZ
    @given(padded_mutibs(max_size=80), st.data())
    def test_mutable_view_from_indices_write_matches_model(self, mb, data):
        m, bits = mb
        ref = bits[:]
        if ref:
            indices = data.draw(
                st.lists(st.integers(0, len(ref) - 1), unique=True, max_size=25),
                label="indices",
            )
        else:
            indices = []
        value = data.draw(
            bool_lists(min_size=len(indices), max_size=len(indices)),
            label="value",
        )

        view = MutableView.from_indices(m, indices)
        view.bin = _ref_bin(value)

        for index, bit in zip(indices, value):
            ref[index] = bit
        assert m.to_bools() == ref

    @FUZZ
    @given(padded_mutibs(min_size=1), st.data())
    def test_inplace_rotate_and_byteswap_match_model(self, mb, data):
        m, bits = mb
        ref = bits[:]
        start = data.draw(st.integers(0, len(ref)), label="start")
        end = data.draw(st.integers(start, len(ref)), label="end")
        n = data.draw(st.integers(0, 2 * len(ref)), label="n")
        m.rotate_left(n, start, end)
        seg = ref[start:end]
        if seg:
            k = n % len(seg)
            ref[start:end] = seg[k:] + seg[:k]
        assert m.to_bools() == ref

        if len(ref) % 8 == 0 and len(ref) > 0:
            m.byte_swap()
            swapped = []
            for chunk_start in range(len(ref) - 8, -8, -8):
                swapped.extend(ref[chunk_start:chunk_start + 8])
            ref = swapped
            assert m.to_bools() == ref

    @FUZZ
    @given(padded_tibs(min_size=1), st.data())
    def test_immutable_set_at_and_inverted_match_model(self, tb, data):
        t, bits = tb
        n = len(bits)
        positions = data.draw(st.lists(st.integers(-n, n - 1), max_size=15), label="positions")

        expected = bits[:]
        for p in positions:
            expected[p] = True
        assert t.set_at(positions).to_bools() == expected

        expected = bits[:]
        for p in positions:
            expected[p] = False
        assert t.unset_at(positions).to_bools() == expected

        expected = bits[:]
        for p in positions:
            expected[p] = not expected[p]
        assert t.inverted(positions).to_bools() == expected


class TestFuzzFoundBugs:
    """Deterministic regressions for bugs found by the fuzz tests above.

    Repeating the same empty container in from_joined used to panic in Rust,
    as did passing empty ranges to the set/unset family of methods.
    """

    def test_from_joined_single_empty_item(self):
        # A single-element list containing an empty Tibs used to panic in
        # Rust ("Chunk width cannot be 0") via the repeated-list fast path.
        assert Tibs.from_joined([Tibs()]) == Tibs()
        empty = Tibs()
        assert Tibs.from_joined([empty, empty, empty]) == Tibs()

    def test_mutibs_from_joined_single_empty_item(self):
        assert Mutibs.from_joined([Mutibs()]) == Mutibs()

    def test_set_empty_ascending_range_is_noop(self):
        # range(1, 0) is empty in Python, so set() should change nothing.
        # It used to panic in Rust ("range 1..0 out of bounds").
        m = Mutibs.from_zeros(3)
        m.set(range(1, 0))
        assert m == Mutibs.from_zeros(3)

    def test_set_empty_descending_range_is_noop(self):
        # range(0, 2, -1) is empty in Python, so set() should change nothing.
        # It used to panic in Rust ("range 3..1 out of bounds").
        m = Mutibs.from_zeros(3)
        m.set(range(0, 2, -1))
        assert m == Mutibs.from_zeros(3)

    def test_unset_empty_descending_range_is_noop(self):
        # This used to raise IndexError ("End of slice out of bounds.")
        # even though the range contains no positions.
        m = Mutibs.from_ones(1)
        m.unset(range(0, 1, -1))
        assert m == Mutibs.from_ones(1)

    def test_set_at_empty_range_is_noop(self):
        # The non-mutating variants route through the same position handling.
        t = Tibs.from_zeros(3)
        assert t.set_at(range(1, 0)) == t
        assert t.unset_at(range(0, 2, -1)) == t

    def test_set_range_negative_values_match_list_semantics(self):
        # A range is interpreted exactly like the list of its contents, so
        # negative values index from the end just as they do in a list.
        m = Mutibs.from_zeros(4)
        m.set(range(-2, 3))  # [-2, -1, 0, 1, 2]
        assert m == Mutibs.from_bools([1, 1, 1, 1])

        m = Mutibs.from_zeros(4)
        m.set(range(-1, -3, -1))  # [-1, -2]
        assert m == Mutibs.from_bools([0, 0, 1, 1])

        m = Mutibs.from_zeros(4)
        m.set(range(3, -5, -1))  # [3, 2, 1, 0, -1, -2, -3, -4]
        assert m == Mutibs.from_ones(4)

        m = Mutibs.from_ones(4)
        m.unset(range(-1, -3, -1))
        assert m == Mutibs.from_bools([1, 1, 0, 0])

    def test_set_range_out_of_bounds_raises_and_leaves_unchanged(self):
        m = Mutibs.from_zeros(4)
        with pytest.raises(IndexError):
            m.set(range(2, 8))
        assert m == Mutibs.from_zeros(4)
        with pytest.raises(IndexError):
            m.set(range(-5, 2))
        assert m == Mutibs.from_zeros(4)
        with pytest.raises(IndexError):
            m.set(range(6, 2, -1))
        assert m == Mutibs.from_zeros(4)
