import gc
import sys
import warnings

import pytest

import wraplib


def test_basic_use():
    with wraplib.Conn("session") as c:
        c.push(5)
        c.push(7)
        assert c.total == 12
    assert c.closed


def test_repr():
    c = wraplib.Conn("session")
    assert repr(c) == "<wraplib.Conn 'session'>"
    c.close()
    assert repr(c) == "<wraplib.Conn 'session' (closed)>"


def test_close_is_idempotent():
    c = wraplib.Conn("x")
    c.close()
    c.close()
    assert c.closed


def test_use_after_close_raises_rather_than_crashing():
    c = wraplib.Conn("x")
    c.close()
    with pytest.raises(ValueError, match="closed"):
        c.push(1)
    with pytest.raises(ValueError, match="closed"):
        _ = c.total
    with pytest.raises(ValueError, match="closed"):
        c.as_capsule()


def test_error_translation():
    with wraplib.Conn("x") as c:
        with pytest.raises(OverflowError):
            c.push(10_000)  # TINY_ERANGE
        with pytest.raises(TypeError):
            c.push("five")
        with pytest.raises(OverflowError):
            c.push(2**40)  # does not fit in a C int


def test_constructor_validation():
    with pytest.raises(TypeError):
        wraplib.Conn(123)
    with pytest.raises(ValueError):
        wraplib.Conn("")  # TINY_EINVAL
    with pytest.raises(OverflowError):
        wraplib.Conn("n" * 100)  # TINY_ERANGE


def test_callback_is_invoked():
    seen = []
    with wraplib.Conn("cb") as c:
        c.callback = seen.append
        c.push(3)
        c.push(4)
    assert seen == [3, 4]


def test_callback_validation():
    with wraplib.Conn("cb") as c:
        with pytest.raises(TypeError):
            c.callback = 42
        c.callback = None
        assert c.callback is None


def test_callback_exception_goes_to_unraisable_hook():
    def boom(_value):
        raise RuntimeError("from the callback")

    caught = []
    previous = sys.unraisablehook
    sys.unraisablehook = caught.append
    try:
        with wraplib.Conn("cb") as c:
            c.callback = boom
            c.push(1)  # must not propagate, must not be silent
            assert c.total == 1
    finally:
        sys.unraisablehook = previous

    assert len(caught) == 1
    assert caught[0].exc_type is RuntimeError
    assert str(caught[0].exc_value) == "from the callback"


def test_callback_cycle_is_collected():
    def live():
        return sum(1 for o in gc.get_objects() if type(o) is wraplib.Conn)

    gc.collect()
    before = live()
    for _ in range(100):
        c = wraplib.Conn("cyclic")
        c.callback = lambda v, c=c: None  # closure referring back to c
        c.close()
        del c
    gc.collect()
    assert live() == before


def test_unclosed_connection_warns():
    with warnings.catch_warnings(record=True) as caught:
        warnings.simplefilter("always")
        wraplib.Conn("forgotten")
        gc.collect()
    assert any(issubclass(w.category, ResourceWarning) for w in caught)


def test_capsule_carries_a_checked_name():
    with wraplib.Conn("x") as c:
        cap = c.as_capsule()
        assert "wraplib.tiny_handle" in repr(cap)


def test_constants_exported():
    assert wraplib.TINY_OK == 0
    assert wraplib.TINY_ERANGE == 3
