import unittest
import gc
from test.support import import_helper

_testcapi = import_helper.import_module('_testcapi')
_testlimitedcapi = import_helper.import_module('_testlimitedcapi')

NULL = None
PY_SSIZE_T_MIN = _testcapi.PY_SSIZE_T_MIN
PY_SSIZE_T_MAX = _testcapi.PY_SSIZE_T_MAX

class TupleSubclass(tuple):
    pass


class CAPITest(unittest.TestCase):
    def _not_tracked(self, t):
        self.assertFalse(gc.is_tracked(t), t)

    def _tracked(self, t):
        self.assertTrue(gc.is_tracked(t), t)

    def test_check(self):
        # Test PyTuple_Check()
        check = _testlimitedcapi.tuple_check

        self.assertTrue(check((1, 2)))
        self.assertTrue(check(()))
        self.assertTrue(check(TupleSubclass((1, 2))))
        self.assertFalse(check({1: 2}))
        self.assertFalse(check([1, 2]))
        self.assertFalse(check(42))
        self.assertFalse(check(object()))

        # CRASHES check(NULL)

    def test_tuple_checkexact(self):
        # Test PyTuple_CheckExact()
        check = _testlimitedcapi.tuple_checkexact

        self.assertTrue(check((1, 2)))
        self.assertTrue(check(()))
        self.assertFalse(check(TupleSubclass((1, 2))))
        self.assertFalse(check({1: 2}))
        self.assertFalse(check([1, 2]))
        self.assertFalse(check(42))
        self.assertFalse(check(object()))

        # CRASHES check(NULL)

    def test_tuple_new(self):
        # Test PyTuple_New()
        tuple_new = _testlimitedcapi.tuple_new
        size = _testlimitedcapi.tuple_size
        checknull = _testcapi._check_tuple_item_is_NULL

        tup1 = tuple_new(0)
        self.assertEqual(tup1, ())
        self.assertEqual(size(tup1), 0)
        self.assertIs(type(tup1), tuple)
        self._not_tracked(tup1)

        tup2 = tuple_new(1)
        self.assertIs(type(tup2), tuple)
        self.assertEqual(size(tup2), 1)
        self.assertIsNot(tup2, tup1)
        self.assertTrue(checknull(tup2, 0))
        self._tracked(tup2)

        self.assertRaises(SystemError, tuple_new, -1)
        self.assertRaises(SystemError, tuple_new, PY_SSIZE_T_MIN)
        self.assertRaises(MemoryError, tuple_new, PY_SSIZE_T_MAX)

    def test_tuple_pack(self):
        # Test PyTuple_Pack()
        pack = _testlimitedcapi.tuple_pack

        self.assertEqual(pack(0), ())
        self.assertEqual(pack(1, [1]), ([1],))
        self.assertEqual(pack(2, [1], [2]), ([1], [2]))

        self._tracked(pack(1, [1]))
        self._tracked(pack(2, [1], b'abc'))
        self._not_tracked(pack(2, 42, b'abc'))

        self.assertRaises(SystemError, pack, PY_SSIZE_T_MIN)
        self.assertRaises(SystemError, pack, -1)
        self.assertRaises(MemoryError, pack, PY_SSIZE_T_MAX)

        # CRASHES pack(1, NULL)
        # CRASHES pack(2, [1])

    def test_tuple_size(self):
        # Test PyTuple_Size()
        size = _testlimitedcapi.tuple_size

        self.assertEqual(size(()), 0)
        self.assertEqual(size((1, 2)), 2)
        self.assertEqual(size(TupleSubclass((1, 2))), 2)

        self.assertRaises(SystemError, size, [])
        self.assertRaises(SystemError, size, 42)
        self.assertRaises(SystemError, size, object())

        # CRASHES size(NULL)

    def test_tuple_get_size(self):
        # Test PyTuple_GET_SIZE()
        size = _testcapi.tuple_get_size

        self.assertEqual(size(()), 0)
        self.assertEqual(size((1, 2)), 2)
        self.assertEqual(size(TupleSubclass((1, 2))), 2)

    def test_tuple_getitem(self):
        # Test PyTuple_GetItem()
        getitem = _testlimitedcapi.tuple_getitem

        tup = ([1], [2], [3])
        self.assertEqual(getitem(tup, 0), [1])
        self.assertEqual(getitem(tup, 2), [3])

        tup2 = TupleSubclass(([1], [2], [3]))
        self.assertEqual(getitem(tup2, 0), [1])
        self.assertEqual(getitem(tup2, 2), [3])

        self.assertRaises(IndexError, getitem, tup, PY_SSIZE_T_MIN)
        self.assertRaises(IndexError, getitem, tup, -1)
        self.assertRaises(IndexError, getitem, tup, len(tup))
        self.assertRaises(IndexError, getitem, tup, PY_SSIZE_T_MAX)
        self.assertRaises(SystemError, getitem, [1, 2, 3], 1)
        self.assertRaises(SystemError, getitem, 42, 1)

        # CRASHES getitem(NULL, 0)

    def test_tuple_get_item(self):
        # Test PyTuple_GET_ITEM()
        get_item = _testcapi.tuple_get_item

        tup = ([1], [2], [3])
        self.assertEqual(get_item(tup, 0), [1])
        self.assertEqual(get_item(tup, 2), [3])

        tup2 = TupleSubclass(([1], [2], [3]))
        self.assertEqual(get_item(tup2, 0), [1])
        self.assertEqual(get_item(tup2, 2), [3])

        # CRASHES get_item(NULL, 0)

    def test_tuple_getslice(self):
        # Test PyTuple_GetSlice()
        getslice = _testlimitedcapi.tuple_getslice

        # empty
        tup = ([1], [2], [3])
        self.assertEqual(getslice(tup, PY_SSIZE_T_MIN, 0), ())
        self.assertEqual(getslice(tup, -1, 0), ())
        self.assertEqual(getslice(tup, 3, PY_SSIZE_T_MAX), ())
        self.assertEqual(getslice(tup, 1, 1), ())
        self.assertEqual(getslice(tup, 2, 1), ())
        tup = TupleSubclass(([1], [2], [3]))
        self.assertEqual(getslice(tup, PY_SSIZE_T_MIN, 0), ())
        self.assertEqual(getslice(tup, -1, 0), ())
        self.assertEqual(getslice(tup, 3, PY_SSIZE_T_MAX), ())
        self.assertEqual(getslice(tup, 1, 1), ())
        self.assertEqual(getslice(tup, 2, 1), ())

        # slice
        tup = ([1], [2], [3], [4])
        self.assertEqual(getslice(tup, 1, 3), ([2], [3]))
        tup = TupleSubclass(([1], [2], [3], [4]))
        self.assertEqual(getslice(tup, 1, 3), ([2], [3]))

        # whole
        tup = ([1], [2], [3])
        self.assertEqual(getslice(tup, 0, 3), tup)
        self.assertEqual(getslice(tup, 0, 100), tup)
        self.assertEqual(getslice(tup, -100, 100), tup)
        tup = TupleSubclass(([1], [2], [3]))
        self.assertEqual(getslice(tup, 0, 3), tup)
        self.assertEqual(getslice(tup, 0, 100), tup)
        self.assertEqual(getslice(tup, -100, 100), tup)

        self.assertRaises(SystemError, getslice, [[1], [2], [3]], 0, 0)
        self.assertRaises(SystemError, getslice, 42, 0, 0)

        # CRASHES getslice(NULL, 0, 0)

    def test_tuple_setitem(self):
        # Test PyTuple_SetItem()
        setitem = _testlimitedcapi.tuple_setitem
        checknull = _testcapi._check_tuple_item_is_NULL

        tup = ([1], [2])
        self.assertEqual(setitem(tup, 0, []), ([], [2]))
        self.assertEqual(setitem(tup, 1, []), ([1], []))

        tup2 = setitem(tup, 1, NULL)
        self.assertTrue(checknull(tup2, 1))

        tup2 = TupleSubclass(([1], [2]))
        self.assertRaises(SystemError, setitem, tup2, 0, [])

        self.assertRaises(IndexError, setitem, tup, PY_SSIZE_T_MIN, [])
        self.assertRaises(IndexError, setitem, tup, -1, [])
        self.assertRaises(IndexError, setitem, tup, len(tup), [])
        self.assertRaises(IndexError, setitem, tup, PY_SSIZE_T_MAX, [])
        self.assertRaises(SystemError, setitem, [1], 0, [])
        self.assertRaises(SystemError, setitem, 42, 0, [])

        # CRASHES setitem(NULL, 0, [])

    def test_tuple_set_item(self):
        # Test PyTuple_SET_ITEM()
        set_item = _testcapi.tuple_set_item
        checknull = _testcapi._check_tuple_item_is_NULL

        tup = ([1], [2])
        self.assertEqual(set_item(tup, 0, []), ([], [2]))
        self.assertEqual(set_item(tup, 1, []), ([1], []))

        tup2 = set_item(tup, 1, NULL)
        self.assertTrue(checknull(tup2, 1))

        tup2 = TupleSubclass(([1], [2]))
        self.assertIs(set_item(tup2, 0, []), tup2)
        self.assertEqual(tup2, ([], [2]))

        # CRASHES set_item(tup, -1, [])
        # CRASHES set_item(tup, len(tup), [])
        # CRASHES set_item([1], 0, [])
        # CRASHES set_item(NULL, 0, [])

    def test__tuple_resize(self):
        # Test _PyTuple_Resize()
        resize = _testcapi._tuple_resize
        checknull = _testcapi._check_tuple_item_is_NULL

        a = ()
        b = resize(a, 0, False)
        self.assertEqual(len(a), 0)
        self.assertEqual(len(b), 0)
        b = resize(a, 2, False)
        self.assertEqual(len(a), 0)
        self.assertEqual(len(b), 2)
        self.assertTrue(checknull(b, 0))
        self.assertTrue(checknull(b, 1))

        a = ([1], [2], [3])
        b = resize(a, 3)
        self.assertEqual(b, a)
        b = resize(a, 2)
        self.assertEqual(b, a[:2])
        b = resize(a, 5)
        self.assertEqual(len(b), 5)
        self.assertEqual(b[:3], a)
        self.assertTrue(checknull(b, 3))
        self.assertTrue(checknull(b, 4))

        a = ()
        self.assertRaises(MemoryError, resize, a, PY_SSIZE_T_MAX)
        self.assertRaises(SystemError, resize, a, -1)
        self.assertRaises(SystemError, resize, a, PY_SSIZE_T_MIN)
        # refcount > 1
        a = (1, 2, 3)
        self.assertRaises(SystemError, resize, a, 3, False)
        self.assertRaises(SystemError, resize, a, 0, False)
        # non-tuple
        self.assertRaises(SystemError, resize, [1, 2, 3], 0, False)
        self.assertRaises(SystemError, resize, NULL, 0, False)

    def test_bug_59313(self):
        # Before 3.14, the C-API function PySequence_Tuple
        # would create incomplete tuples which were visible to
        # the cycle GC, and this test would crash the interpeter.
        TAG = object()
        tuples = []

        def referrer_tuples():
            return [x for x in gc.get_referrers(TAG)
                if isinstance(x, tuple)]

        def my_iter():
            nonlocal tuples
            yield TAG    # 'tag' gets stored in the result tuple
            tuples += referrer_tuples()
            for x in range(10):
                tuples += referrer_tuples()
                # Prior to 3.13 would raise a SystemError when the tuple needs to be resized
                yield x

        self.assertEqual(tuple(my_iter()), (TAG, *range(10)))
        self.assertEqual(tuples, [])


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