import operator
import random
import pytest

# Ajusta este import a tu proyecto
from hugenat import HugeNat

import numpy as np
from numba import njit

# ----------------------------
# Oráculos de referencia (int)
# ----------------------------

def ref_bit(x: int, i: int) -> int:
    """
    Oráculo de referencia para leer un bit de un entero Python.

    - Solo admite naturales (x >= 0). Si no, lanza ValueError.
    - Usa la misma convención que HugeNat:
      * i >= 0 indexa desde el bit menos significativo (LSB = bit 0).
      * i < 0 indexa desde el bit más significativo del número (relativo a bit_length).
    - Si el índice cae fuera del rango de bits representados (0..nbits-1), devuelve 0.

    Devuelve 0 o 1 como entero Python.
    """
    if x < 0:
        raise ValueError
    nbits = x.bit_length()
    if i < 0:
        if nbits == 0:
            raise IndexError
        i += nbits
    if i < 0 or i >= nbits:
        return 0
    return (x >> i) & 1


def ref_slice_step1(x: int, start, stop) -> int:
    """
    Oráculo de referencia para slicing rápido (step None/1) sobre un entero Python.

    Emula la semántica que queremos para el fast-path de HugeNat:
    - Interpreta `start` y `stop` como lo hace Python (incluye negativos relativos a bit_length).
    - Luego recorta el intervalo a [0, nbits] para evitar “bits infinitos a 0”.
    - Extrae los bits en el rango [start, stop) y los recompacta:
      el bit `start` pasa a ser el bit 0 del resultado.
    - Si el slice queda vacío (start >= stop), devuelve 0.

    Devuelve un entero Python con el valor extraído.
    """
    if x < 0:
        raise ValueError
    nbits = x.bit_length()

    if start is None:
        start = 0
    if stop is None:
        stop = nbits

    if start < 0:
        start += nbits
    if stop < 0:
        stop += nbits

    start = max(0, min(start, nbits))
    stop = max(0, min(stop, nbits))

    if start >= stop:
        return 0

    width = stop - start
    mask = (1 << width) - 1
    return (x >> start) & mask


def ref_slice_general(x: int, sl: slice) -> int:
    """
    Oráculo de referencia para slicing general (slow-path) sobre un entero Python.

    Aquí forzamos semántica Python completa de slicing:
    - Construimos la lista de bits del entero (LSB -> MSB) como una secuencia finita.
    - Aplicamos el slice real de Python `bits[sl]` (incluye step > 1 y step < 0).
    - Recompactamos el resultado en un nuevo entero:
      el primer bit extraído pasa a ser el bit 0 del entero resultante.

    Notas:
    - x debe ser natural (x >= 0).
    - Si sl.step == 0, lanza ValueError (igual que Python).
    - Como la secuencia de bits es finita (hasta bit_length), cualquier acceso fuera de rango
      se comporta como slicing de listas (no como bits infinitos).
    """
    if x < 0:
        raise ValueError

    if sl.step == 0:
        raise ValueError

    nbits = x.bit_length()
    bits = [(x >> i) & 1 for i in range(nbits)]  # LSB -> MSB
    picked = bits[sl]

    out = 0
    for j, b in enumerate(picked):
        out |= (b & 1) << j
    return out


def rand_nat(nbits: int) -> int:
    """
    Genera un natural aleatorio con hasta `nbits` bits.

    - Si nbits <= 0, devuelve 0.
    - Si nbits > 0, usa `random.getrandbits(nbits)` para obtener un entero uniforme en
      [0, 2**nbits).

    Se usa en tests de tipo property-based (muchas combinaciones rápidas).
    """
    if nbits <= 0:
        return 0
    return random.getrandbits(nbits)

# ----------------------------
# Numba helper: slicing fast sobre core (limbs, nbits)
# ----------------------------


@njit
def _u64_bit_length(x):
    """
    Versión Numba-friendly de bit_length para un uint64.

    Numba no puede llamar de forma directa a `int.bit_length()` sobre uint64 como en Python,
    así que calculamos el número de bits significativos a mano:
    - 0 -> 0
    - si x != 0 -> contamos cuántos shifts a la derecha hacen falta para llegar a 0.
    """
    bl = 0
    while x != 0:
        x >>= 1
        bl += 1
    return bl


@njit
def _trim_nbits(limbs):
    """
    Calcula `nbits` canónico a partir de un array de palabras uint64 (limbs).

    `limbs` representa la magnitud en little-endian por palabra:
    - limbs[0] contiene bits 0..63 (LSB chunk)
    - limbs[1] contiene bits 64..127, etc.

    Esta función:
    - Busca desde el final la última palabra no nula (para eliminar ceros líderes).
    - Calcula cuántos bits significativos tiene esa palabra (bit_length uint64).
    - Devuelve el `nbits` total: (índice_palabra * 64 + bit_length(palabra_top)).

    Si todas las palabras son 0 o el array está vacío, devuelve 0.
    """
    n = limbs.size
    if n == 0:
        return np.int64(0)

    i = n - 1
    while i >= 0 and limbs[i] == 0:
        i -= 1
    if i < 0:
        return np.int64(0)

    return np.int64(i * 64 + _u64_bit_length(limbs[i]))


@njit
def slice_bits_fast_core(limbs, nbits, start, stop):
    """
    Extrae un rango de bits [start, stop) desde una representación por limbs (core),
    usando un algoritmo de “ventana” eficiente por palabras (fast-path).

    Contrato:
    - Se asume step = 1 (el caller ya eligió fast-path).
    - `limbs` es uint64 contiguo, little-endian por palabra.
    - `nbits` es el número de bits significativos del valor original.
    - `start` y `stop` llegan ya como enteros (sin None), pero aquí se recortan a [0, nbits].
    - El resultado se recompacta: el bit `start` pasa a ser el bit 0 del resultado.
    - Si el intervalo queda vacío, devuelve (array vacío, 0).

    Implementación (idea):
    - Localiza la palabra inicial `word0 = start // 64` y el desplazamiento dentro de palabra.
    - Copia/combina palabras sucesivas para aplicar el shift y reconstruir el bloque alineado.
    - Enmascara la última palabra para truncar a `width` bits.
    - Recalcula `out_nbits` quitando ceros líderes (trim).
    """
    if start < 0:
        start = 0
    if stop < 0:
        stop = 0
    if start > nbits:
        start = nbits
    if stop > nbits:
        stop = nbits
    if start >= stop:
        return np.empty(0, dtype=np.uint64), np.int64(0)

    width = stop - start
    out_words = (width + 63) >> 6

    word0 = start >> 6
    shift = start & 63

    out = np.empty(out_words, dtype=np.uint64)

    if shift == 0:
        for i in range(out_words):
            s = word0 + i
            out[i] = limbs[s] if s < limbs.size else np.uint64(0)
    else:
        rshift = shift
        lshift = 64 - shift
        for i in range(out_words):
            s0 = word0 + i
            s1 = s0 + 1
            lo = limbs[s0] if s0 < limbs.size else np.uint64(0)
            hi = limbs[s1] if s1 < limbs.size else np.uint64(0)
            out[i] = (lo >> rshift) | (hi << lshift)

    rem = width & 63
    if rem != 0:
        out[-1] &= (np.uint64(1) << rem) - np.uint64(1)

    out_nbits = _trim_nbits(out)
    if out_nbits == 0:
        return np.empty(0, dtype=np.uint64), np.int64(0)

    need = (out_nbits + 63) >> 6
    return out[:need].copy(), out_nbits


# ----------------------------
# 1) Dominio y construcción
# ----------------------------

def test_domain_rejects_negative():
    """Verifica que no se pueda crear un HugeNat con un entero negativo."""
    with pytest.raises(ValueError):
        HugeNat(-1)


def test_declared_bit_length_is_optional_and_default_behavior_remains():
    """Verifica que sin bit_length explícito el comportamiento sea el de siempre."""
    x = HugeNat(0b1011)
    assert int(x) == 0b1011
    assert x.bit_length() == 4


def test_declared_bit_length_pads_with_leading_zeros():
    """Verifica que bit_length explícito mayor añada ceros lógicos a la izquierda."""
    x = HugeNat(0b1011, bit_length=8)
    assert int(x) == 0b1011
    assert x.bit_length() == 8
    assert x[-1] == 0
    assert x[-8] == 1


def test_declared_bit_length_truncates_high_bits():
    """Verifica que bit_length explícito menor trunque bits altos."""
    x = HugeNat(0b110101, bit_length=4)
    assert int(x) == 0b0101
    assert x.bit_length() == 4


def test_declared_bit_length_validation():
    """Verifica validación de tipo y rango para bit_length explícito."""
    with pytest.raises(TypeError):
        HugeNat(1, bit_length=1.5)
    with pytest.raises(TypeError):
        HugeNat(1, bit_length=True)
    with pytest.raises(ValueError):
        HugeNat(1, bit_length=-1)


@pytest.mark.parametrize("v", [0, 1, 2, 3, 7, 8, 2 ** 63, 2 ** 127 + 12345])
def test_int_and_index_roundtrip(v):
    """Comprueba que int(), index() y bool() reflejan el valor original."""
    x = HugeNat(v)
    assert int(x) == v
    assert operator.index(x) == v
    assert bool(x) == (v != 0)


def test_hash_matches_int():
    """Comprueba que el hash de HugeNat coincide con el hash de int."""
    x = HugeNat(123456789)
    assert hash(x) == hash(int(x))


def test_str_matches_int():
    """Comprueba que str(HugeNat) devuelve el mismo texto que str(int)."""
    v = 2 ** 80 + 7
    x = HugeNat(v)
    assert str(x) == str(v)


# ----------------------------
# 2) Paridad API int (mínimos)
# ----------------------------

@pytest.mark.parametrize("v", [0, 1, 2, 3, 255, 256, 2 ** 100 + 3])
def test_bit_length_and_count_match_int(v):
    """Verifica que bit_length() y bit_count() coincidan con int."""
    x = HugeNat(v)
    assert x.bit_length() == v.bit_length()
    assert x.bit_count() == v.bit_count()


def test_to_bytes_from_bytes_roundtrip():
    """Verifica que convertir a bytes y volver conserve el valor."""
    v = 2 ** 200 + 2 ** 64 + 7
    x = HugeNat(v)
    length = (v.bit_length() + 7) // 8
    b = x.to_bytes(length=length, byteorder="big", signed=False)
    y = HugeNat.from_bytes(b, byteorder="big", signed=False)
    assert int(y) == v


# ----------------------------
# 3) Aritmética/bitwise y retorno
# ----------------------------

def test_basic_ops_with_hugenat_return_hugenat():
    """Comprueba operaciones básicas entre HugeNat y que devuelven HugeNat."""
    a = HugeNat(10)
    b = HugeNat(7)

    r1 = a + b
    r2 = a * b
    r3 = a << 5
    r4 = a | b
    r5 = a & b
    r6 = a ^ b
    r7 = a // b
    r8 = a % b
    r9 = pow(a, 3)

    for r in (r1, r2, r3, r4, r5, r6, r7, r8, r9):
        assert isinstance(r, HugeNat)

    assert int(r1) == 17
    assert int(r2) == 70
    assert int(r3) == 10 << 5
    assert int(r4) == (10 | 7)
    assert int(r5) == (10 & 7)
    assert int(r6) == (10 ^ 7)
    assert int(r7) == (10 // 7)
    assert int(r8) == (10 % 7)
    assert int(r9) == (10 ** 3)


def test_ops_with_python_int():
    """Comprueba operaciones básicas entre HugeNat e int de Python."""
    x = HugeNat(123)
    assert isinstance(x + 7, HugeNat)
    assert isinstance(7 + x, HugeNat)
    assert int(x + 7) == 130
    assert int(7 + x) == 130
    assert int(x & 0b1010) == (123 & 0b1010)
    assert int(0b1010 & x) == (0b1010 & 123)


@pytest.mark.parametrize(
    "op",
    [
        lambda x: x + (-1),
        lambda x: (-1) + x,
        lambda x: x * (-1),
        lambda x: (-1) * x,
        lambda x: x // (-1),
        lambda x: (-1) // x,
        lambda x: x % (-1),
        lambda x: (-1) % x,
        lambda x: x & (-1),
        lambda x: (-1) & x,
        lambda x: x | (-1),
        lambda x: (-1) | x,
        lambda x: x ^ (-1),
        lambda x: (-1) ^ x,
        lambda x: x - (-1),
        lambda x: (-1) - x,
    ],
)
def test_ops_reject_negative_operands(op):
    """Verifica que las operaciones rechacen operandos negativos."""
    with pytest.raises(ValueError):
        _ = op(HugeNat(5))


def test_bitwise_ops_and_shifts():
    """Verifica resultados y tipos en bitwise y desplazamientos."""
    a = HugeNat(0b1100)
    b = HugeNat(0b1010)

    assert isinstance(a & b, HugeNat)
    assert isinstance(a | b, HugeNat)
    assert isinstance(a ^ b, HugeNat)

    assert int(a & b) == (0b1100 & 0b1010)
    assert int(a | b) == (0b1100 | 0b1010)
    assert int(a ^ b) == (0b1100 ^ 0b1010)

    assert isinstance(a << 2, HugeNat)
    assert isinstance(a >> 1, HugeNat)
    assert int(a << 2) == (0b1100 << 2)
    assert int(a >> 1) == (0b1100 >> 1)


def test_bit_rotations():
    """Comprueba rotaciones de bits y el caso especial del valor cero."""
    x = HugeNat(0b100101)
    assert int(x.rotl(2)) == 0b010110
    assert int(x.rotr(2)) == 0b011001

    # Ancho cero devuelve 0
    z = HugeNat(0)
    assert int(z.rotl(5)) == 0
    assert int(z.rotr(5)) == 0


def test_bit_rotations_use_declared_bit_length():
    """Verifica que las rotaciones respeten la anchura lógica declarada."""
    x = HugeNat(0b1, bit_length=4)
    assert int(x.rotl(1)) == 0b0010
    assert int(x.rotr(1)) == 0b1000


def test_init_from_array_uint64():
    """Verifica construcción desde limbs uint64 en little-endian."""
    limbs = np.array([0xFFFFFFFFFFFFFFFF, 0x1], dtype=np.uint64)
    x = HugeNat(limbs)
    assert int(x) == (1 << 64) | 0xFFFFFFFFFFFFFFFF


def test_init_from_array_ints():
    """Verifica construcción desde una lista de limbs enteros (BE: el primer elemento es el MS)."""
    limbs = [1, 2, 3]
    x = HugeNat(limbs)
    assert int(x) == (1 << 128) | (2 << 64) | 3


def test_arrays_with_leading_zero_limb_are_equivalent():
    """Comprueba que ceros de mayor peso al inicio (BE) no cambian el valor."""
    a = HugeNat([0xAAAAAAAAAAAAAAAA, 0xAAAAAAAAAAAAAAAA, 0])  # cero LS → valor distinto
    b = HugeNat([0xAAAAAAAAAAAAAAAA, 0xAAAAAAAAAAAAAAAA])
    c = HugeNat([0, 0xAAAAAAAAAAAAAAAA, 0xAAAAAAAAAAAAAAAA])  # cero MS → mismo valor que b

    assert a != b
    assert b == c


def test_float_array_fractional_rejected():
    """Verifica que limbs con parte decimal se rechacen."""
    arr = np.array([1.5, 2.0], dtype=float)
    with pytest.raises(ValueError):
        HugeNat(arr)


def test_float_array_integer_like_accepted():
    """Verifica que floats exactos tipo entero se acepten."""
    arr = np.array([1.0, 2.0, 3.0], dtype=float)
    x = HugeNat(arr)
    assert int(x) == (3 << 128) | (2 << 64) | 1


def test_float_array_large_rejected_for_precision():
    """Verifica rechazo de floats grandes con riesgo de pérdida de precisión."""
    arr = np.array([2 ** 60, 2 ** 61], dtype=float)
    with pytest.raises(ValueError):
        HugeNat(arr)


def test_eq_invalid_array_like_returns_false():
    """Comprueba que __eq__ devuelve False si el array no es válido."""
    assert (HugeNat(1) == [1.5]) is False
    assert (HugeNat(1) == ["not a number"]) is False


def test_subtraction_cannot_go_negative():
    """Verifica que restar y quedar en negativo lance ValueError."""
    with pytest.raises(ValueError):
        _ = HugeNat(3) - HugeNat(5)
    with pytest.raises(ValueError):
        _ = HugeNat(3) - 5


def test_invert_flips_bits_within_logical_width():
    """Verifica NOT bit a bit dentro del ancho lógico."""
    # Con bit_length fijo
    x = HugeNat(0b1011, bit_length=4)
    assert int(~x) == 0b0100
    assert (~x).bit_length() == 4

    # Natural (sin bit_length fijo): usa bit_length() natural
    y = HugeNat(0b1011)           # bit_length natural = 4
    assert int(~y) == 0b0100
    assert (~y).bit_length() == 4

    # Todo ceros con ancho fijo -> todo unos
    z = HugeNat(0, bit_length=8)
    assert int(~z) == 0xFF
    assert (~z).bit_length() == 8

    # Todo unos con ancho fijo -> todo ceros
    w = HugeNat(0xFF, bit_length=8)
    assert int(~w) == 0
    assert (~w).bit_length() == 8


def test_invert_double_negation_is_identity():
    """~~x == x para cualquier valor y ancho."""
    for v, bl in [(0b1011, 4), (0, 8), (0xFF, 8), (0xDEADBEEF, 32)]:
        x = HugeNat(v, bit_length=bl)
        assert int(~~x) == int(x)
        assert (~~x).bit_length() == x.bit_length()

    # Sin bit_length fijo
    y = HugeNat(0b101101)
    assert int(~~y) == int(y)


def test_invert_zero_without_declared_width_returns_zero():
    """~HugeNat(0) sin ancho declarado devuelve HugeNat(0, bit_length=0)."""
    r = ~HugeNat(0)
    assert int(r) == 0
    assert r.bit_length() == 0


def test_invert_preserves_width_on_large_values():
    """NOT bit a bit funciona en valores de más de 64 bits."""
    v = (1 << 128) - 1            # 128 unos
    x = HugeNat(v, bit_length=128)
    assert int(~x) == 0
    assert (~x).bit_length() == 128

    x2 = HugeNat(0, bit_length=128)
    assert int(~x2) == v
    assert (~x2).bit_length() == 128


# ----------------------------
# 4) Indexado por bits
# ----------------------------

@pytest.mark.parametrize("v", [0, 1, 2, 3, 0xDEADBEEF, 2 ** 130 + 5])
def test_bit_indexing_matches_reference(v: int):
    """Comprueba el indexado de bits contra el oráculo de referencia."""
    x = HugeNat(v)
    n = v.bit_length()

    if n == 0:
        assert x[0] == 0
        assert x[-1] == 0
        return

    # puntos típicos
    for i in (0, 1, 2, n - 1):
        assert x[i] == ref_bit(v, i)

    assert x[-1] == ref_bit(v, -1)
    assert x[-n] == ref_bit(v, -n)

    assert x[n] == 0
    assert x[-(n + 1)] == 0


def test_bit_indexing_uses_declared_bit_length():
    """Verifica indexado negativo usando la anchura lógica fijada."""
    x = HugeNat(0b1, bit_length=4)
    assert x[-1] == 0
    assert x[-4] == 1
    assert x[4] == 0


# ----------------------------
# 4.5) Split por trozos iguales
# ----------------------------

def test_split_divides_in_equal_chunks_lsb_to_msb():
    """Verifica split en trozos iguales con orden de salida LSB->MSB."""
    x = HugeNat(0b1011010110010001, bit_length=16)
    parts = x.split(4)

    assert len(parts) == 4
    assert all(isinstance(p, HugeNat) for p in parts)
    assert [int(p) for p in parts] == [0b0001, 0b1001, 0b0101, 0b1011]
    assert [p.bit_length() for p in parts] == [4, 4, 4, 4]


def test_split_uses_declared_bit_length_and_keeps_zero_chunks():
    """Verifica split con anchura declarada y trozos altos en cero."""
    x = HugeNat(0b1011, bit_length=8)
    low, high = x.split(2)

    assert int(low) == 0b1011
    assert int(high) == 0
    assert low.bit_length() == 4
    assert high.bit_length() == 4
    assert high[-1] == 0


def test_split_requires_divisible_bit_length():
    """Verifica que split falle si bit_length no es divisible por n."""
    with pytest.raises(ValueError):
        HugeNat(0b10101).split(2)


def test_split_validates_n_argument():
    """Verifica validación de tipo y rango del argumento n."""
    x = HugeNat(0b1011)
    with pytest.raises(TypeError):
        x.split(True)
    with pytest.raises(TypeError):
        x.split(1.5)
    with pytest.raises(ValueError):
        x.split(0)
    with pytest.raises(ValueError):
        x.split(-1)


def test_split_allows_zero_width_when_declared_bit_length_is_zero():
    """Verifica split sobre anchura lógica cero."""
    x = HugeNat(0, bit_length=0)
    parts = x.split(3)
    assert len(parts) == 3
    assert [int(p) for p in parts] == [0, 0, 0]
    assert [p.bit_length() for p in parts] == [0, 0, 0]


def test_split_with_32bit_values_in_64bit_limbs_keeps_interleaved_zero_chunks():
    """Verifica que valores de 32 bits en limbs de 64 dejen huecos de 32 ceros al partir."""
    low32 = 0xFFFFFFFF
    x = HugeNat([low32, low32], bit_length=128)
    parts = x.split(4)

    assert len(parts) == 4
    assert [int(p) for p in parts] == [low32, 0, low32, 0]
    assert [p.bit_length() for p in parts] == [32, 32, 32, 32]


def test_split_with_full_64bit_limbs_returns_four_full_32bit_chunks():
    """Verifica que dos limbs completos de 64 bits produzcan cuatro bloques de 32 bits llenos."""
    full64 = 0xFFFFFFFFFFFFFFFF
    low32 = 0xFFFFFFFF
    x = HugeNat([full64, full64], bit_length=128)
    parts = x.split(4)

    assert len(parts) == 4
    assert [int(p) for p in parts] == [low32, low32, low32, low32]
    assert [p.bit_length() for p in parts] == [32, 32, 32, 32]


# ----------------------------
# 4b) append – concatenación de bits
# ----------------------------

def test_append_basic_concatenation():
    """append concatena los bits de B sobre los de A."""
    a = HugeNat(0b11, bit_length=2)   # bits: 11
    b = HugeNat(0b01, bit_length=2)   # bits: 01
    r = a.append(b)
    # resultado: 01 || 11 = 0b0111
    assert int(r) == 0b0111
    assert r.bit_length() == 4


def test_append_respects_leading_zeros():
    """Los ceros altos declarados se preservan en la concatenación."""
    a = HugeNat(0b1, bit_length=4)    # 0001
    b = HugeNat(0b1, bit_length=4)    # 0001
    r = a.append(b)
    # resultado: 0001 || 0001 = 00010001
    assert int(r) == 0b00010001
    assert r.bit_length() == 8


def test_append_multiple_operands():
    """append acepta varios argumentos y los apila en orden."""
    a = HugeNat(0b10, bit_length=2)
    b = HugeNat(0b01, bit_length=2)
    c = HugeNat(0b11, bit_length=2)
    r = a.append(b, c)
    # resultado: 11 || 01 || 10 = 0b110110
    assert int(r) == 0b110110
    assert r.bit_length() == 6


def test_append_is_inverse_of_split():
    """split seguido de append reconstruye el valor original."""
    x = HugeNat(0xDEADBEEFCAFEBABE, bit_length=64)
    parts = x.split(4)  # 4 trozos de 16 bits
    reconstructed = parts[0].append(*parts[1:])
    assert int(reconstructed) == int(x)
    assert reconstructed.bit_length() == 64


def test_append_with_zero_bit_length_operands():
    """Operandos con bit_length=0 no aportan bits."""
    a = HugeNat(0b101, bit_length=3)
    z = HugeNat(0, bit_length=0)
    r = a.append(z)
    assert int(r) == 0b101
    assert r.bit_length() == 3


def test_append_all_zeros():
    """Concatenar ceros produce cero con ancho acumulado."""
    a = HugeNat(0, bit_length=4)
    b = HugeNat(0, bit_length=4)
    r = a.append(b)
    assert int(r) == 0
    assert r.bit_length() == 8


def test_append_without_fixed_bit_length():
    """Sin bit_length fijo, usa el bit_length natural."""
    a = HugeNat(0b111)   # bit_length natural = 3
    b = HugeNat(0b1)     # bit_length natural = 1
    r = a.append(b)
    # 1 || 111 = 0b1111
    assert int(r) == 0b1111
    assert r.bit_length() == 4


def test_append_requires_at_least_one_argument():
    """append sin argumentos lanza TypeError."""
    a = HugeNat(1)
    with pytest.raises(TypeError):
        a.append()


def test_append_rejects_non_hugenat():
    """append rechaza tipos que no son HugeNat."""
    a = HugeNat(1)
    with pytest.raises(TypeError):
        a.append(42)
    with pytest.raises(TypeError):
        a.append(HugeNat(1), 0)


def test_append_large_values():
    """append funciona con valores grandes (>64 bits)."""
    a = HugeNat((1 << 128) - 1, bit_length=128)
    b = HugeNat(1, bit_length=1)
    r = a.append(b)
    expected = ((1 << 128) - 1) | (1 << 128)
    assert int(r) == expected
    assert r.bit_length() == 129


# ----------------------------
# 5) Slicing fast-path (step None/1)
# ----------------------------

@pytest.mark.parametrize("v, sl", [(0, slice(0, 1, None)),
                                   (0b101101, slice(0, 3, None)),
                                   (0b101101, slice(2, 5, None)),
                                   (0b101101, slice(None, None, None)),
                                   (0b101101, slice(-3, None, None)),
                                   (0b101101, slice(None, -1, None)),
                                   (2 ** 200 + 12345, slice(17, 140, None)),
                                   ],
                         )
def test_slice_step1_matches_reference(v, sl):
    """Comprueba slicing con step 1/None contra el oráculo rápido."""
    x = HugeNat(v)
    out = x[sl]
    assert isinstance(out, HugeNat)
    assert int(out) == ref_slice_step1(v, sl.start, sl.stop)


def test_slice_empty_returns_zero_not_error():
    """Verifica que un slice vacío devuelva 0 y no lance error."""
    x = HugeNat(0b101101)
    assert int(x[3:3]) == 0
    assert int(x[4:2]) == 0


def test_slice_step1_uses_declared_bit_length_by_default():
    """Verifica que un slice completo use la anchura lógica declarada."""
    x = HugeNat(0b1011, bit_length=8)
    out = x[:]
    assert int(out) == 0b1011


# ----------------------------
# 6) Slicing slow-path (step arbitrario)
# ----------------------------

@pytest.mark.parametrize("v, sl", [(0, slice(None, None, -1)),
                                   (0b101101, slice(None, None, 2)),
                                   (0b101101, slice(5, None, -1)),
                                   (0b101101, slice(4, 0, -2)),
                                   (2 ** 120 + 0b101101, slice(None, None, 3)),
                                   ],
                         )
def test_slice_general_step_matches_reference(v, sl):
    """Comprueba slicing general con step arbitrario contra oráculo."""
    x = HugeNat(v)
    out = x[sl]
    assert isinstance(out, HugeNat)
    assert int(out) == ref_slice_general(v, sl)


def test_slice_step_zero_raises():
    """Verifica que usar step=0 en slicing lance ValueError."""
    x = HugeNat(123)
    with pytest.raises(ValueError):
        _ = x[0:10:0]


def test_slice_negative_step_can_include_zeros_above_declared_bit_length():
    """Verifica relleno con ceros cuando el slice empieza por encima de bit_length."""
    x = HugeNat(0b1011, bit_length=4)
    out = x[6:2:-1]
    assert int(out) == 0b1000


# ----------------------------
# 7) bits() view
# ----------------------------

@pytest.mark.parametrize("v", [0, 1, 2, 3, 0b101101, 2 ** 100 + 7])
def test_bits_view_default_msb_to_lsb(v):
    """Comprueba bits() por defecto en orden msb->lsb."""
    x = HugeNat(v)
    arr = np.asarray(x.bits(), dtype=np.uint8)

    if v == 0:
        # contrato estricto: 0 -> vista vacía
        assert arr.size == 0
        return

    # msb->lsb: primer bit siempre 1 (MSB), último es LSB
    assert arr[0] == 1
    assert arr[-1] == (v & 1)


def test_bits_view_length_padding():
    """Verifica que bits(length=...) haga padding correctamente."""
    x = HugeNat(0b101)
    arr = np.asarray(x.bits(length=8), dtype=np.uint8)
    assert arr.size == 8
    # msb->lsb con padding a 8: "00000101"
    assert arr.tolist() == [0, 0, 0, 0, 0, 1, 0, 1]


def test_bits_view_order_lsb_to_msb():
    """Comprueba bits() cuando se pide orden lsb->msb."""
    v = 0b101101
    x = HugeNat(v)
    arr = np.asarray(x.bits(order="lsb->msb"), dtype=np.uint8)
    # lsb->msb: arr[0] es LSB
    assert arr[0] == (v & 1)
    assert arr[-1] == 1  # MSB


def test_bits_view_respects_declared_bit_length():
    """Verifica que bits() incluya ceros de la anchura lógica fijada."""
    x = HugeNat(0b101, bit_length=8)
    arr = np.asarray(x.bits(), dtype=np.uint8)
    assert arr.tolist() == [0, 0, 0, 0, 0, 1, 0, 1]


# ----------------------------
# 8) Propiedades aleatorias: slice step1
# ----------------------------

@pytest.mark.parametrize("nbits", [0, 1, 2, 5, 64, 65, 130, 257])
def test_property_slice_step1_random(nbits):
    """Prueba aleatoria: slice step1 debe coincidir con el oráculo."""
    v = rand_nat(nbits)
    x = HugeNat(v)
    n = v.bit_length()

    for _ in range(50):
        if n == 0:
            a = random.randint(-5, 5)
            b = random.randint(-5, 5)
        else:
            a = random.randint(-n - 5, n + 5)
            b = random.randint(-n - 5, n + 5)

        sl = slice(a, b, None)
        assert int(x[sl]) == ref_slice_step1(v, sl.start, sl.stop)


# ----------------------------
# 8b) Correcciones de revisión general
# ----------------------------

def test_rotl_rotr_preserve_fixed_bit_length():
    """Verifica que las rotaciones preserven la anchura lógica fijada."""
    x = HugeNat(0b0001, bit_length=4)
    rl = x.rotl(1)
    rr = x.rotr(1)
    assert rl.bit_length() == 4
    assert rr.bit_length() == 4
    assert int(rl) == 0b0010
    assert int(rr) == 0b1000

    # Caso zero-width
    z = HugeNat(0, bit_length=0)
    assert z.rotl(3).bit_length() == 0
    assert z.rotr(3).bit_length() == 0


def test_rotl_rotr_reject_non_int_with_typeerror():
    """Verifica que rotl/rotr lancen TypeError para shift no int."""
    x = HugeNat(0b1010)
    with pytest.raises(TypeError):
        x.rotl(1.5)
    with pytest.raises(TypeError):
        x.rotr("2")


def test_rpow():
    """Verifica que 2 ** HugeNat(3) devuelva HugeNat(8)."""
    r = 2 ** HugeNat(3)
    assert isinstance(r, HugeNat)
    assert int(r) == 8


def test_abs_and_pos():
    """Verifica __abs__ y __pos__ para naturales."""
    x = HugeNat(42)
    assert isinstance(abs(x), HugeNat)
    assert int(abs(x)) == 42
    assert isinstance(+x, HugeNat)
    assert int(+x) == 42

    # Preservan bit_length fijo
    y = HugeNat(0b1, bit_length=8)
    assert abs(y).bit_length() == 8
    assert (+y).bit_length() == 8


def test_divmod_and_rdivmod():
    """Verifica divmod con HugeNat."""
    a = HugeNat(17)
    b = HugeNat(5)
    q, r = divmod(a, b)
    assert isinstance(q, HugeNat)
    assert isinstance(r, HugeNat)
    assert int(q) == 3
    assert int(r) == 2

    # Reflejado
    q2, r2 = divmod(17, b)
    assert isinstance(q2, HugeNat)
    assert int(q2) == 3
    assert int(r2) == 2


def test_rsub_valid_case():
    """Verifica resta reflejada con resultado no negativo."""
    r = 10 - HugeNat(3)
    assert isinstance(r, HugeNat)
    assert int(r) == 7


def test_bits_str_zero_returns_zero():
    """Verifica que bits_str() para cero devuelva '0'."""
    assert HugeNat(0).bits_str() == "0"
    assert HugeNat(0, bit_length=0).bits_str() == "0"


# ----------------------------
# 9) Numba: el core debe ser usable en njit
# ----------------------------

def test_to_core_contract_and_roundtrip_from_core():
    """Verifica contrato de to_core y reconstrucción exacta con from_core."""
    v = 2 ** 200 + 2 ** 130 + 123456789
    x = HugeNat(v)

    limbs, nbits = x.to_core()

    assert isinstance(limbs, np.ndarray)
    assert limbs.dtype == np.uint64
    assert limbs.flags["C_CONTIGUOUS"]
    assert isinstance(nbits, (np.integer, int))
    assert int(nbits) == v.bit_length()

    y = HugeNat.from_core(limbs, nbits)
    assert int(y) == v


def test_to_core_keeps_declared_bit_length_and_zero_words():
    """Verifica que to_core conserve anchura lógica y limbs de relleno en cero."""
    x = HugeNat(0b101, bit_length=130)
    limbs, nbits = x.to_core()

    assert int(nbits) == 130
    assert limbs.dtype == np.uint64
    assert limbs.size == 3
    assert int(limbs[0]) == 0b101
    assert int(limbs[1]) == 0
    assert int(limbs[2]) == 0


def test_from_core_accepts_declared_width_with_zero_top_limb():
    """Verifica que from_core acepte anchura fija aunque el limb alto sea cero."""
    limbs = np.array([0], dtype=np.uint64)
    x = HugeNat.from_core(limbs, 1)
    assert int(x) == 0
    assert x.bit_length() == 1
    assert x[-1] == 0


def test_from_core_rejects_invalid_types_and_layout():
    """Verifica que from_core rechace tipos y layout inválidos."""
    with pytest.raises(TypeError):
        HugeNat.from_core([1], 1)
    with pytest.raises(TypeError):
        HugeNat.from_core(np.array([1], dtype=np.uint64), 1.5)
    with pytest.raises(ValueError):
        HugeNat.from_core(np.array([1], dtype=np.int64), 1)

    non_contig = np.array([1, 2, 3, 4], dtype=np.uint64)[::2]
    assert not non_contig.flags["C_CONTIGUOUS"]
    with pytest.raises(ValueError):
        HugeNat.from_core(non_contig, 65)


def test_from_core_rejects_inconsistent_or_out_of_range_core():
    """Verifica que from_core rechace core inconsistente o con bits fuera de nbits."""
    with pytest.raises(ValueError):
        HugeNat.from_core(np.array([1], dtype=np.uint64), 0)
    with pytest.raises(ValueError):
        HugeNat.from_core(np.empty(0, dtype=np.uint64), 1)
    with pytest.raises(ValueError):
        HugeNat.from_core(np.array([1], dtype=np.uint64), 65)
    with pytest.raises(ValueError):
        HugeNat.from_core(np.array([2], dtype=np.uint64), 1)


def test_numba_core_fast_slice_matches_class_slice():
    """Comprueba que el slicing fast del core coincida con HugeNat."""
    v = 2 ** 200 + 2 ** 130 + 123456789
    x = HugeNat(v)
    limbs, nbits = x.to_core()

    # Casos variados (solo step1)
    cases = [
        (0, 16),
        (8, 80),
        (63, 129),
        (17, 140),
        (140, 17),  # vacío
        (-10, None),  # None se resuelve fuera
        (None, -1),
    ]

    n = int(nbits)
    for a, b in cases:
        sl = slice(a, b, None)

        # Normalización para pasar a core: usa el mismo criterio del contrato fast-path
        start = 0 if sl.start is None else sl.start
        stop = n if sl.stop is None else sl.stop

        if start < 0:
            start += n
        if stop < 0:
            stop += n

        out_limbs, out_nbits = slice_bits_fast_core(limbs, nbits, start, stop)
        y = HugeNat.from_core(out_limbs, out_nbits)

        assert int(y) == int(x[sl])


# ----------------------------
# 10) Negación, float, bool en shifts
# ----------------------------

def test_neg_raises_value_error():
    """Verifica que -HugeNat levante ValueError con mensaje claro (no TypeError genérico)."""
    with pytest.raises(ValueError):
        _ = -HugeNat(5)
    with pytest.raises(ValueError):
        _ = -HugeNat(0)


def test_float_conversion():
    """Verifica que float(HugeNat) funcione correctamente."""
    assert float(HugeNat(0)) == 0.0
    assert float(HugeNat(5)) == 5.0
    assert float(HugeNat(2 ** 53)) == float(2 ** 53)
    assert isinstance(float(HugeNat(100)), float)


def test_init_from_exact_float():
    """Verifica que HugeNat(n.0) funcione para floats exactamente enteros."""
    assert int(HugeNat(0.0)) == 0
    assert int(HugeNat(3.0)) == 3
    assert int(HugeNat(2.0 ** 53)) == 2 ** 53


def test_init_from_float_rejects_fractional():
    """Verifica que HugeNat(3.5) lance ValueError."""
    with pytest.raises(ValueError):
        HugeNat(3.5)
    with pytest.raises(ValueError):
        HugeNat(0.1)


def test_init_from_float_rejects_negative():
    """Verifica que HugeNat(-1.0) lance ValueError."""
    with pytest.raises(ValueError):
        HugeNat(-1.0)
    with pytest.raises(ValueError):
        HugeNat(-0.5)


def test_init_from_float_rejects_too_large():
    """Verifica que floats mayores de 2**53 se rechacen por pérdida de precisión."""
    with pytest.raises(ValueError):
        HugeNat(float(2 ** 54))


def test_init_from_float_rejects_inf_and_nan():
    """Verifica que inf y nan se rechacen al construir un HugeNat."""
    with pytest.raises(ValueError):
        HugeNat(float("inf"))
    with pytest.raises(ValueError):
        HugeNat(float("nan"))


def test_lshift_rshift_reject_bool():
    """Verifica que << y >> rechacen bool como shift (consistencia con bit_length)."""
    x = HugeNat(5)
    with pytest.raises(TypeError):
        _ = x << True
    with pytest.raises(TypeError):
        _ = x >> True
    with pytest.raises(TypeError):
        _ = x << False
    with pytest.raises(TypeError):
        _ = x >> False


def test_rotl_rotr_reject_bool():
    """Verifica que rotl y rotr rechacen bool como shift (consistencia con bit_length)."""
    x = HugeNat(0b1010, bit_length=4)
    with pytest.raises(TypeError):
        x.rotl(True)
    with pytest.raises(TypeError):
        x.rotr(True)
    with pytest.raises(TypeError):
        x.rotl(False)
    with pytest.raises(TypeError):
        x.rotr(False)


# ----------------------------
# 11) Cache de limbs y to_core(copy, word_order)
# ----------------------------

def test_to_core_copy_false_returns_readonly_view():
    """to_core(copy=False) devuelve vista read-only de la cache."""
    x = HugeNat(0xDEADBEEF, bit_length=64)
    limbs1, nb1 = x.to_core(copy=False)
    limbs2, nb2 = x.to_core(copy=False)
    assert not limbs1.flags["WRITEABLE"]
    # Ambas vistas apuntan a la misma memoria
    assert np.shares_memory(limbs1, limbs2)
    assert np.array_equal(limbs1, limbs2)


def test_to_core_copy_true_returns_independent_copy():
    """to_core(copy=True) devuelve copia independiente."""
    x = HugeNat(0xDEADBEEF, bit_length=64)
    limbs1, _ = x.to_core(copy=True)
    limbs2, _ = x.to_core(copy=True)
    assert limbs1.flags["WRITEABLE"]
    assert not np.shares_memory(limbs1, limbs2)


def test_to_core_word_order_be():
    """to_core(word_order='be') devuelve limbs en orden big-endian."""
    x = HugeNat([0x1111111111111111, 0x2222222222222222], bit_length=128)
    le_limbs, _ = x.to_core(word_order="le")
    be_limbs, _ = x.to_core(word_order="be")
    assert np.array_equal(be_limbs, le_limbs[::-1])


def test_to_core_invalid_word_order_raises():
    """to_core con word_order inválido lanza ValueError."""
    x = HugeNat(1)
    with pytest.raises(ValueError):
        x.to_core(word_order="xx")


def test_from_core_word_order_be_roundtrip():
    """from_core(word_order='be') reconstruye correctamente."""
    x = HugeNat([0xAAAA, 0xBBBB], bit_length=128)
    be_limbs, nbits = x.to_core(word_order="be")
    y = HugeNat.from_core(be_limbs, nbits, word_order="be")
    assert int(y) == int(x)
    assert y.bit_length() == x.bit_length()


def test_from_core_word_order_le_is_default():
    """from_core sin word_order funciona como antes (LE)."""
    x = HugeNat(0xCAFEBABE, bit_length=64)
    limbs, nbits = x.to_core()
    y = HugeNat.from_core(limbs, nbits)
    assert int(y) == int(x)


# ----------------------------
# 12) split_core() y split_u64()
# ----------------------------

def test_split_core_matches_split_to_core():
    """split_core produce los mismos limbs que split() + to_core() por trozo."""
    x = HugeNat(0xDEADBEEFCAFEBABE, bit_length=64)
    parts_obj = x.split(4)
    core_2d = x.split_core(4)

    assert core_2d.shape[0] == 4
    for i, p in enumerate(parts_obj):
        limbs_p, _ = p.to_core()
        # Pad to same width for comparison
        row = core_2d[i]
        expected = np.zeros_like(row)
        expected[:limbs_p.size] = limbs_p
        assert np.array_equal(row, expected)


def test_split_core_word_order_be():
    """split_core con word_order='be' invierte cada fila."""
    x = HugeNat((1 << 128) - 1, bit_length=128)
    le = x.split_core(2, word_order="le")
    be = x.split_core(2, word_order="be")
    assert np.array_equal(be[0], le[0, ::-1])
    assert np.array_equal(be[1], le[1, ::-1])


def test_split_core_validates_args():
    """split_core valida argumentos como split."""
    x = HugeNat(0b1011, bit_length=4)
    with pytest.raises(TypeError):
        x.split_core(True)
    with pytest.raises(ValueError):
        x.split_core(0)
    with pytest.raises(ValueError):
        x.split_core(3)  # 4 no divisible por 3
    with pytest.raises(ValueError):
        x.split_core(2, word_order="xx")


def test_split_u64_matches_split():
    """split_u64 equivalencia con split() para chunks <= 64 bits."""
    x = HugeNat(0xDEADBEEFCAFEBABE, bit_length=64)
    parts_obj = x.split(4)
    u64_arr = x.split_u64(4)

    assert u64_arr.dtype == np.uint64
    assert u64_arr.shape == (4,)
    for i, p in enumerate(parts_obj):
        assert int(u64_arr[i]) == int(p)


def test_split_u64_rejects_large_chunks():
    """split_u64 rechaza chunks > 64 bits."""
    x = HugeNat((1 << 128) - 1, bit_length=128)
    with pytest.raises(ValueError):
        x.split_u64(1)  # 128 bits > 64


def test_split_u64_zero_width():
    """split_u64 con bit_length=0 devuelve ceros."""
    x = HugeNat(0, bit_length=0)
    arr = x.split_u64(3)
    assert arr.shape == (3,)
    assert np.all(arr == 0)


# ----------------------------
# 13) extract_bits_core() y extract_bits_u64()
# ----------------------------

def test_extract_bits_core_matches_slicing():
    """extract_bits_core equivalencia con slicing + to_core."""
    v = 2 ** 200 + 2 ** 130 + 123456789
    x = HugeNat(v)

    for start, width in [(0, 64), (17, 128), (63, 65), (130, 71)]:
        core_arr = x.extract_bits_core(start, width)
        sliced = x[start:start + width]
        sliced_limbs, _ = sliced.to_core()
        # Pad for comparison
        expected = np.zeros_like(core_arr)
        expected[:sliced_limbs.size] = sliced_limbs
        assert np.array_equal(core_arr, expected), f"Mismatch at start={start}, width={width}"


def test_extract_bits_core_zero_width():
    """extract_bits_core con width=0 devuelve array vacío."""
    x = HugeNat(0xFF)
    arr = x.extract_bits_core(0, 0)
    assert arr.size == 0
    assert arr.dtype == np.uint64


def test_extract_bits_core_word_order_be():
    """extract_bits_core con word_order='be'."""
    x = HugeNat((1 << 128) - 1, bit_length=128)
    le = x.extract_bits_core(0, 128, word_order="le")
    be = x.extract_bits_core(0, 128, word_order="be")
    assert np.array_equal(be, le[::-1])


def test_extract_bits_core_validates_args():
    """extract_bits_core valida argumentos."""
    x = HugeNat(0xFF)
    with pytest.raises(ValueError):
        x.extract_bits_core(-1, 8)
    with pytest.raises(ValueError):
        x.extract_bits_core(0, -1)
    with pytest.raises(ValueError):
        x.extract_bits_core(0, 8, word_order="xx")


def test_extract_bits_u64_matches_slicing():
    """extract_bits_u64 para rangos <= 64 bits."""
    v = 0xDEADBEEFCAFEBABE
    x = HugeNat(v, bit_length=64)

    for start, width in [(0, 16), (16, 32), (48, 16), (0, 64)]:
        result = x.extract_bits_u64(start, width)
        expected = (v >> start) & ((1 << width) - 1)
        assert int(result) == expected


def test_extract_bits_u64_rejects_large_width():
    """extract_bits_u64 rechaza width > 64."""
    x = HugeNat((1 << 128) - 1, bit_length=128)
    with pytest.raises(ValueError):
        x.extract_bits_u64(0, 65)


def test_extract_bits_u64_zero_width():
    """extract_bits_u64 con width=0 devuelve 0."""
    x = HugeNat(0xFF)
    assert int(x.extract_bits_u64(0, 0)) == 0


# ----------------------------
# 14) Preservación de _fixed_nbits en bitwise ops
# ----------------------------

def test_bitwise_and_preserves_fixed_nbits():
    """AND preserva _fixed_nbits cuando ambos operandos coinciden."""
    a = HugeNat(0xFF, bit_length=8)
    b = HugeNat(0x0F, bit_length=8)
    r = a & b
    assert int(r) == 0x0F
    assert r.bit_length() == 8
    assert r._fixed_nbits == 8


def test_bitwise_or_preserves_fixed_nbits():
    """OR preserva _fixed_nbits."""
    a = HugeNat(0x0F, bit_length=8)
    b = HugeNat(0xF0, bit_length=8)
    r = a | b
    assert int(r) == 0xFF
    assert r.bit_length() == 8


def test_bitwise_xor_preserves_fixed_nbits():
    """XOR preserva _fixed_nbits."""
    a = HugeNat(0xFF, bit_length=8)
    b = HugeNat(0xFF, bit_length=8)
    r = a ^ b
    assert int(r) == 0
    assert r.bit_length() == 8
    assert r._fixed_nbits == 8


def test_lshift_preserves_fixed_nbits_with_mask():
    """lshift preserva _fixed_nbits y aplica máscara."""
    a = HugeNat(0x80, bit_length=8)
    r = a << 1
    assert r._fixed_nbits == 8
    assert int(r) == 0  # 0x100 masked to 8 bits = 0


def test_rshift_preserves_fixed_nbits():
    """rshift preserva _fixed_nbits."""
    a = HugeNat(0xFF, bit_length=8)
    r = a >> 4
    assert r._fixed_nbits == 8
    assert int(r) == 0x0F
    assert r.bit_length() == 8


def test_bitwise_ops_no_fixed_nbits_unchanged():
    """Sin _fixed_nbits, el comportamiento es el mismo de siempre."""
    a = HugeNat(0xFF)
    b = HugeNat(0x0F)
    r = a & b
    assert r._fixed_nbits is None
    assert int(r) == 0x0F


def test_bitwise_self_fixed_other_not():
    """Solo self con _fixed_nbits → resultado con ese ancho."""
    a = HugeNat(0xFF, bit_length=8)
    r = a & 0x0F
    assert r._fixed_nbits == 8
    assert int(r) == 0x0F


def test_bitwise_different_fixed_nbits():
    """Ambos con _fixed_nbits distintos → sin ancho fijo."""
    a = HugeNat(0xFF, bit_length=8)
    b = HugeNat(0xFF, bit_length=16)
    r = a & b
    assert r._fixed_nbits is None


# ----------------------------
# 15) Numba kernels
# ----------------------------

from hugenat.numba_core import split_equal_parts_core, extract_range_core, concat_parts_core


def test_numba_split_equal_parts_matches_python():
    """Kernel split_equal_parts_core produce resultados idénticos a split_core."""
    x = HugeNat(0xDEADBEEFCAFEBABE, bit_length=64)
    limbs, nbits = x.to_core()

    py_result = x.split_core(4)
    nb_result = split_equal_parts_core(limbs, int(nbits), 4)

    assert np.array_equal(py_result, nb_result)


def test_numba_extract_range_matches_python():
    """Kernel extract_range_core produce resultados idénticos a extract_bits_core."""
    v = 2 ** 200 + 2 ** 130 + 123456789
    x = HugeNat(v)
    limbs, nbits = x.to_core()

    for start, width in [(0, 64), (17, 128), (63, 65)]:
        py_arr = x.extract_bits_core(start, width)
        nb_arr, nb_nbits = extract_range_core(limbs, int(nbits), start, width)
        # Pad for comparison
        expected = np.zeros(py_arr.size, dtype=np.uint64)
        expected[:nb_arr.size] = nb_arr
        assert np.array_equal(py_arr, expected), f"Mismatch at start={start}, width={width}"


def test_numba_concat_parts_roundtrip():
    """concat_parts_core es inversa de split_equal_parts_core."""
    x = HugeNat(0xDEADBEEFCAFEBABE, bit_length=64)
    limbs, nbits = x.to_core()

    parts_2d = split_equal_parts_core(limbs, int(nbits), 4)
    recon_limbs, recon_nbits = concat_parts_core(parts_2d, 16)

    y = HugeNat.from_core(recon_limbs, recon_nbits)
    assert int(y) == int(x)


def test_numba_split_extract_large_value():
    """Kernels Numba funcionan con valores grandes (> 128 bits)."""
    v = (1 << 256) - 1
    x = HugeNat(v, bit_length=256)
    limbs, nbits = x.to_core()

    parts = split_equal_parts_core(limbs, int(nbits), 4)
    assert parts.shape == (4, 1)  # 64 bits each = 1 word

    extracted, enb = extract_range_core(limbs, int(nbits), 64, 128)
    expected = (v >> 64) & ((1 << 128) - 1)
    recon = HugeNat.from_core(extracted, enb)
    assert int(recon) == expected


# ----------------------------
# 16) R1: to_words_be / from_words_be
# ----------------------------

def test_to_words_be_matches_to_core_be():
    """to_words_be() es consistente con to_core(word_order='be')."""
    x = HugeNat([0x1111111111111111, 0x2222222222222222], bit_length=128)
    be_words = x.to_words_be()
    be_limbs, _ = x.to_core(word_order="be")
    assert np.array_equal(be_words, be_limbs)


def test_from_words_be_roundtrip():
    """from_words_be → to_words_be roundtrip."""
    v = (1 << 192) - 7
    x = HugeNat(v, bit_length=192)
    be = x.to_words_be()
    y = HugeNat.from_words_be(be, 192)
    assert int(y) == int(x)
    assert y.bit_length() == 192


def test_from_words_be_to_words_be_consistency():
    """to_words_be produce el mismo array que from_words_be recibe."""
    v = 0xDEADBEEFCAFEBABE
    x = HugeNat(v, bit_length=64)
    be = x.to_words_be()
    y = HugeNat.from_words_be(be, 64)
    assert int(y) == v
    be2 = y.to_words_be()
    assert np.array_equal(be, be2)


def test_from_words_be_validates_input():
    """from_words_be valida tipos y dimensiones."""
    with pytest.raises(TypeError):
        HugeNat.from_words_be([1, 2], 128)
    with pytest.raises(ValueError):
        HugeNat.from_words_be(np.array([[1]], dtype=np.uint64), 64)
    with pytest.raises(ValueError):
        HugeNat.from_words_be(np.array([1], dtype=np.uint32), 32)


def test_to_words_be_zero():
    """to_words_be de cero devuelve array vacío."""
    x = HugeNat(0, bit_length=0)
    be = x.to_words_be()
    assert be.size == 0


# ----------------------------
# 17) R2: split_array
# ----------------------------

def test_split_array_default_be():
    """split_array con default word_order='be'."""
    x = HugeNat((1 << 128) - 1, bit_length=128)
    be_parts = x.split_array(2)
    le_parts = x.split_core(2, word_order="be")
    assert np.array_equal(be_parts, le_parts)


def test_split_array_le():
    """split_array con word_order='le' equivale a split_core."""
    x = HugeNat(0xDEADBEEFCAFEBABE, bit_length=64)
    arr_le = x.split_array(4, word_order="le")
    core_le = x.split_core(4, word_order="le")
    assert np.array_equal(arr_le, core_le)


def test_split_array_vs_manual_conversion():
    """split_array('be') produce mismos datos que split + to_core(be) por trozo."""
    x = HugeNat((1 << 256) - 1, bit_length=256)
    parts_obj = x.split(4)
    arr_be = x.split_array(4, word_order="be")

    assert arr_be.shape[0] == 4
    for i, p in enumerate(parts_obj):
        limbs_be, _ = p.to_core(word_order="be")
        row = arr_be[i]
        expected = np.zeros_like(row)
        n = min(limbs_be.size, expected.size)
        expected[expected.size - n:] = limbs_be[limbs_be.size - n:]
        assert np.array_equal(row, expected)


# ----------------------------
# 18) R3: bitwise_or, shift_left, extract_bits (static, BE)
# ----------------------------

from hugenat.numba_core import bitwise_or_core, shift_left_core


def test_bitwise_or_core_basic():
    """bitwise_or_core kernel: OR de dos arrays LE."""
    a = np.array([0xFF00, 0x0], dtype=np.uint64)
    b = np.array([0x00FF], dtype=np.uint64)
    out, nbits = bitwise_or_core(a, b)
    assert int(out[0]) == 0xFFFF
    assert out.size <= 2


def test_bitwise_or_static_be():
    """HugeNat.bitwise_or sobre arrays BE."""
    va = 0xF0F0F0F0
    vb = 0x0F0F0F0F
    a = HugeNat(va, bit_length=64)
    b = HugeNat(vb, bit_length=64)
    a_be = a.to_words_be()
    b_be = b.to_words_be()
    result_be = HugeNat.bitwise_or(a_be, b_be, 64)
    y = HugeNat.from_words_be(result_be, 64)
    assert int(y) == va | vb


def test_bitwise_or_static_large():
    """HugeNat.bitwise_or con valores > 64 bits."""
    va = (1 << 128) - 1
    vb = (1 << 64)
    a = HugeNat(va, bit_length=128)
    b = HugeNat(vb, bit_length=128)
    a_be = a.to_words_be()
    b_be = b.to_words_be()
    result_be = HugeNat.bitwise_or(a_be, b_be, 128)
    y = HugeNat.from_words_be(result_be, 128)
    assert int(y) == va | vb


def test_shift_left_core_basic():
    """shift_left_core kernel: shift left sobre array LE."""
    limbs = np.array([0x1], dtype=np.uint64)
    out, nbits = shift_left_core(limbs, 4)
    assert int(out[0]) == 0x10


def test_shift_left_static_be():
    """HugeNat.shift_left sobre array BE."""
    v = 0x1
    x = HugeNat(v, bit_length=64)
    x_be = x.to_words_be()
    result_be = HugeNat.shift_left(x_be, 8, 64)
    y = HugeNat.from_words_be(result_be, 64)
    assert int(y) == (v << 8) & ((1 << 64) - 1)


def test_shift_left_static_truncates():
    """HugeNat.shift_left trunca al bit_length dado."""
    v = 0x8000000000000000  # bit 63 set
    x = HugeNat(v, bit_length=64)
    x_be = x.to_words_be()
    result_be = HugeNat.shift_left(x_be, 1, 64)
    y = HugeNat.from_words_be(result_be, 64)
    assert int(y) == 0  # bit 63 shifted out


def test_shift_left_static_cross_word():
    """HugeNat.shift_left cruzando límite de palabra."""
    v = 0xFFFFFFFFFFFFFFFF
    x = HugeNat(v, bit_length=128)
    x_be = x.to_words_be()
    result_be = HugeNat.shift_left(x_be, 32, 128)
    y = HugeNat.from_words_be(result_be, 128)
    assert int(y) == (v << 32) & ((1 << 128) - 1)


def test_extract_bits_static_be():
    """HugeNat.extract_bits sobre array BE."""
    v = 0xDEADBEEFCAFEBABE
    x = HugeNat(v, bit_length=64)
    x_be = x.to_words_be()
    result_be = HugeNat.extract_bits(x_be, 16, 32)
    expected = (v >> 16) & ((1 << 32) - 1)
    y = HugeNat.from_words_be(result_be, 32)
    assert int(y) == expected


def test_extract_bits_static_large():
    """HugeNat.extract_bits sobre valores > 64 bits."""
    v = (1 << 256) - 1
    x = HugeNat(v, bit_length=256)
    x_be = x.to_words_be()
    result_be = HugeNat.extract_bits(x_be, 64, 128)
    expected = (v >> 64) & ((1 << 128) - 1)
    y = HugeNat.from_words_be(result_be, 128)
    assert int(y) == expected


def test_bitwise_or_matches_hugenat_op():
    """bitwise_or estático produce mismo resultado que operador | de HugeNat."""
    va = (1 << 200) + 12345
    vb = (1 << 180) + 67890
    a = HugeNat(va, bit_length=256)
    b = HugeNat(vb, bit_length=256)
    expected = int(a | b)
    result_be = HugeNat.bitwise_or(a.to_words_be(), b.to_words_be(), 256)
    y = HugeNat.from_words_be(result_be, 256)
    assert int(y) == expected


def test_shift_left_matches_hugenat_op():
    """shift_left estático produce mismo resultado que operador << de HugeNat."""
    v = (1 << 100) + 999
    x = HugeNat(v, bit_length=256)
    for k in [0, 1, 7, 63, 64, 65, 127, 128]:
        expected = (v << k) & ((1 << 256) - 1)
        result_be = HugeNat.shift_left(x.to_words_be(), k, 256)
        y = HugeNat.from_words_be(result_be, 256)
        assert int(y) == expected, f"Mismatch for k={k}"


# ----------------------------
# 19) R5: concat_array
# ----------------------------

def test_concat_array_roundtrip_be():
    """concat_array(split_array(...)) roundtrip con BE."""
    v = (1 << 256) - 1
    x = HugeNat(v, bit_length=256)
    parts_be = x.split_array(4, word_order="be")
    recon_be = HugeNat.concat_array(parts_be, 64, word_order="be")
    y = HugeNat.from_words_be(recon_be, 256)
    assert int(y) == v


def test_concat_array_roundtrip_le():
    """concat_array(split_core(...)) roundtrip con LE."""
    v = (1 << 128) - 7
    x = HugeNat(v, bit_length=128)
    parts_le = x.split_core(2, word_order="le")
    recon_le = HugeNat.concat_array(parts_le, 64, word_order="le")
    y = HugeNat.from_core(recon_le, 128, word_order="le")
    assert int(y) == v


def test_concat_array_validates_input():
    """concat_array valida tipos y dimensiones."""
    with pytest.raises(ValueError):
        HugeNat.concat_array(np.array([1], dtype=np.uint64), 64, word_order="be")
    with pytest.raises(TypeError):
        HugeNat.concat_array([[1, 2]], 64, word_order="be")
    with pytest.raises(ValueError):
        HugeNat.concat_array(np.zeros((2, 1), dtype=np.uint64), 64, word_order="xx")


def test_concat_array_arbitrary_value():
    """concat_array preserva valor arbitrario en roundtrip."""
    v = 0xDEADBEEFCAFEBABE0123456789ABCDEF
    x = HugeNat(v, bit_length=128)
    parts_be = x.split_array(2, word_order="be")
    recon_be = HugeNat.concat_array(parts_be, 64, word_order="be")
    y = HugeNat.from_words_be(recon_be, 128)
    assert int(y) == v
