import array
import threading

import pytest

import bufsum


def doubles(*values):
    return array.array("d", values)


def test_sum():
    assert bufsum.sum_doubles(doubles(1.0, 2.0, 3.5)) == 6.5
    assert bufsum.sum_doubles(doubles()) == 0.0


def test_sum_rejects_wrong_element_type():
    with pytest.raises(TypeError, match="C doubles"):
        bufsum.sum_doubles(array.array("i", [1, 2, 3]))


def test_sum_rejects_non_buffer():
    with pytest.raises(TypeError):
        bufsum.sum_doubles([1.0, 2.0])  # a list is not a buffer exporter


def test_sum_rejects_non_finite():
    with pytest.raises(OverflowError):
        bufsum.sum_doubles(doubles(float("inf"), 1.0))


def test_scale_in_place():
    a = doubles(1.0, 2.0, 3.0)
    assert bufsum.scale(a, 2.0) is None
    assert list(a) == [2.0, 4.0, 6.0]


def test_scale_requires_writable():
    ro = memoryview(doubles(1.0, 2.0)).toreadonly()
    with pytest.raises(BufferError):
        bufsum.scale(ro, 2.0)


def test_checksum_accepts_any_bytes_like():
    h = bufsum.checksum(b"hello")
    assert h == bufsum.checksum(bytearray(b"hello"))
    assert h == bufsum.checksum(memoryview(b"hello"))
    assert h != bufsum.checksum(b"hellp")


def test_buffer_pin_is_real():
    ba = bytearray(b"x" * 16)
    mv = memoryview(ba)
    with pytest.raises(BufferError):
        ba.extend(b"more")  # refused while a view exists
    del mv
    ba.extend(b"more")


def test_non_contiguous_is_rejected():
    np = pytest.importorskip("numpy")
    arr = np.arange(12, dtype=np.float64).reshape(3, 4)
    with pytest.raises((ValueError, BufferError)):
        bufsum.sum_doubles(arr.T)
    assert bufsum.sum_doubles(np.ascontiguousarray(arr.T).ravel()) == arr.sum()


def test_releases_the_interpreter():
    """Two threads summing large buffers should overlap, not serialize.

    This is a smoke test, not a benchmark: it only asserts that the work
    completes, since timing assertions are flaky in CI.
    """
    data = array.array("d", [1.0]) * 1_000_000
    results = []

    def work():
        results.append(bufsum.sum_doubles(data))

    ts = [threading.Thread(target=work) for _ in range(4)]
    for t in ts:
        t.start()
    for t in ts:
        t.join()
    assert results == [1_000_000.0] * 4
