""" Tests for the compiler components of the Numba typed-list.

The tests here should exercise everything within an `@njit` context.
Importantly, the tests should not return a typed list from within such a
context as this would require code from numba/typed/typedlist.py (this is
tested separately).  Tests in this file build on each other in the order of
writing. For example, the first test, tests the creation, append and len of the
list. These are the barebones to do anything useful with a list. The subsequent
test for getitem assumes makes use of these three operations and therefore
assumes that they work.

"""

from textwrap import dedent

from numba import njit
from numba import int32
from numba.extending import register_jitable
from numba.core import types
from numba.core.errors import TypingError
from numba.tests.support import (TestCase, MemoryLeakMixin, override_config,
                                 forbid_codegen)
from numba.typed import listobject


class TestCreateAppendLength(MemoryLeakMixin, TestCase):
    """Test list creation, append and len. """

    def test_list_create(self):
        @njit
        def foo(n):
            l = listobject.new_list(int32)
            for i in range(n):
                l.append(i)
            return len(l)

        for i in (0, 1, 2, 100):
            self.assertEqual(foo(i), i)

    def test_list_create_no_jit(self):
        with override_config('DISABLE_JIT', True):
            with forbid_codegen():
                l = listobject.new_list(int32)
                self.assertEqual(type(l), list)


class TestBool(MemoryLeakMixin, TestCase):
    """Test list bool."""

    def test_list_bool(self):
        @njit
        def foo(n):
            l = listobject.new_list(int32)
            for i in range(n):
                l.append(i)
            return bool(l)

        for i in (0, 1, 2, 100):
            self.assertEqual(foo(i), i > 0)


class TestAllocation(MemoryLeakMixin, TestCase):

    def test_list_allocation(self):
        @njit
        def foo_kwarg(n):
            l = listobject.new_list(int32, allocated=n)
            return l._allocated()

        for i in range(16):
            self.assertEqual(foo_kwarg(i), i)

        @njit
        def foo_posarg(n):
            l = listobject.new_list(int32, n)
            return l._allocated()
        for i in range(16):
            self.assertEqual(foo_posarg(i), i)

    def test_list_allocation_negative(self):
        @njit
        def foo():
            l = listobject.new_list(int32, -1)
            return l._allocated()

        with self.assertRaises(RuntimeError) as raises:
            self.assertEqual(foo(), -1)
        self.assertIn(
            "expecting *allocated* to be >= 0",
            str(raises.exception),
        )


class TestToFromMeminfo(MemoryLeakMixin, TestCase):

    def test_list_to_from_meminfo(self):
        """
        Exercise listobject.{_as_meminfo, _from_meminfo}
        """

        @njit
        def boxer():
            l = listobject.new_list(int32)
            for i in range(10, 20):
                l.append(i)
            return listobject._as_meminfo(l)

        lsttype = types.ListType(int32)

        @njit
        def unboxer(mi):
            l = listobject._from_meminfo(mi, lsttype)
            return l[0], l[1], l[2], l[3], l[4], l[5], l[6], l[7], l[8], l[9]

        mi = boxer()
        self.assertEqual(mi.refcount, 1)

        received = list(unboxer(mi))
        expected = list(range(10, 20))
        self.assertEqual(received, expected)


class TestGetitem(MemoryLeakMixin, TestCase):
    """Test list getitem. """

    def test_list_getitem_singleton(self):
        @njit
        def foo(n):
            l = listobject.new_list(int32)
            l.append(n)
            return l[0]

        self.assertEqual(foo(0), 0)

    def test_list_getitem_singleton_negtive_index(self):
        @njit
        def foo(n):
            l = listobject.new_list(int32)
            l.append(n)
            return l[-1]

        self.assertEqual(foo(0), 0)

    def test_list_getitem_multiple(self):
        @njit
        def foo(i):
            l = listobject.new_list(int32)
            for j in range(10, 20):
                l.append(j)
            return l[i]

        for i,j in ((0, 10), (9, 19), (4, 14), (-5, 15), (-1, 19), (-10, 10)):
            self.assertEqual(foo(i), j)

    def test_list_getitem_empty_index_error(self):
        self.disable_leak_check()

        @njit
        def foo(i):
            l = listobject.new_list(int32)
            return l[i]

        for i in (1, 0, -1):
            with self.assertRaises(IndexError) as raises:
                foo(i)
            self.assertIn(
                "list index out of range",
                str(raises.exception),
            )

    def test_list_getitem_multiple_index_error(self):
        self.disable_leak_check()

        @njit
        def foo(i):
            l = listobject.new_list(int32)
            for j in range(10, 20):
                l.append(j)
            return l[i]

        for i in (10, -11):
            with self.assertRaises(IndexError) as raises:
                foo(i)
            self.assertIn(
                "list index out of range",
                str(raises.exception),
            )

    def test_list_getitem_empty_typing_error(self):
        self.disable_leak_check()

        @njit
        def foo(i):
            l = listobject.new_list(int32)
            return l[i]

        for i in "xyz", 1.0, 1j:
            with self.assertRaises(TypingError) as raises:
                foo(i)
            self.assertIn(
                "list indices must be integers or slices",
                str(raises.exception),
            )

    def test_list_getitem_integer_types_as_index(self):

        @njit
        def foo(i):
            l = listobject.new_list(int32)
            l.append(0)
            return l[i]

        # try all signed integers and make sure they are cast
        for t in (types.signed_domain
                  ):
            self.assertEqual(foo((t(0))), 0)

    def test_list_getitem_different_sized_int_index(self):
        # Checks that the index type cast and ext/trunc to the
        # type of the length is correct, both wraparound and
        # direct index is tested via -1/0.

        for ty in types.integer_domain:
            @njit
            def foo():
                l = listobject.new_list(int32)
                l.append(7)
                return l[ty(0)], l[ty(-1)]

            self.assertEqual(foo(), (7, 7))


class TestGetitemSlice(MemoryLeakMixin, TestCase):
    """Test list getitem when indexing with slices. """

    def test_list_getitem_empty_slice_defaults(self):
        @njit
        def foo():
            l = listobject.new_list(int32)
            n = l[:]
            return len(n)

        self.assertEqual(foo(), 0)

    def test_list_getitem_singleton_slice_defaults(self):
        @njit
        def foo():
            l = listobject.new_list(int32)
            l.append(0)
            n = l[:]
            return len(n)

        self.assertEqual(foo(), 1)

    def test_list_getitem_multiple_slice_defaults(self):
        @njit
        def foo(i):
            l = listobject.new_list(int32)
            for j in range(10, 20):
                l.append(j)
            n = l[:]
            return n[i]

        for i,j in ((0, 10), (9, 19), (4, 14), (-5, 15), (-1, 19), (-10, 10)):
            self.assertEqual(foo(i), j)

    def test_list_getitem_multiple_slice_pos_start(self):
        @njit
        def foo():
            l = listobject.new_list(int32)
            for j in range(10, 20):
                l.append(j)
            n = l[5:]
            return len(n), (n[0], n[1], n[2], n[3], n[4])

        length, items = foo()
        self.assertEqual(length, 5)
        self.assertEqual(items, (15, 16, 17, 18, 19))

    def test_list_getitem_multiple_slice_pos_stop(self):
        @njit
        def foo():
            l = listobject.new_list(int32)
            for j in range(10, 20):
                l.append(j)
            n = l[:5]
            return len(n), (n[0], n[1], n[2], n[3], n[4])

        length, items = foo()
        self.assertEqual(length, 5)
        self.assertEqual(items, (10, 11, 12, 13, 14))

    def test_list_getitem_multiple_slice_pos_start_pos_stop(self):
        @njit
        def foo():
            l = listobject.new_list(int32)
            for j in range(10, 20):
                l.append(j)
            n = l[2:7]
            return len(n), (n[0], n[1], n[2], n[3], n[4])

        length, items = foo()
        self.assertEqual(length, 5)
        self.assertEqual(items, (12, 13, 14, 15, 16))

    def test_list_getitem_multiple_slice_pos_start_pos_stop_pos_step(self):
        @njit
        def foo():
            l = listobject.new_list(int32)
            for j in range(10, 20):
                l.append(j)
            n = l[1:9:2]
            return len(n), (n[0], n[1], n[2], n[3])

        length, items = foo()
        self.assertEqual(length, 4)
        self.assertEqual(items, (11, 13, 15, 17))

    def test_list_getitem_multiple_slice_neg_start(self):
        @njit
        def foo():
            l = listobject.new_list(int32)
            for j in range(10, 20):
                l.append(j)
            n = l[-5:]
            return len(n), (n[0], n[1], n[2], n[3], n[4])

        length, items = foo()
        self.assertEqual(length, 5)
        self.assertEqual(items, (15, 16, 17, 18, 19))

    def test_list_getitem_multiple_slice_neg_stop(self):
        @njit
        def foo():
            l = listobject.new_list(int32)
            for j in range(10, 20):
                l.append(j)
            n = l[:-5]
            return len(n), (n[0], n[1], n[2], n[3], n[4])

        length, items = foo()
        self.assertEqual(length, 5)
        self.assertEqual(items, (10, 11, 12, 13, 14))

    def test_list_getitem_multiple_slice_neg_step(self):
        @njit
        def foo():
            l = listobject.new_list(int32)
            for j in range(10, 20):
                l.append(j)
            n = l[::-2]
            return len(n), (n[0], n[1], n[2], n[3], n[4])

        length, items = foo()
        self.assertEqual(length, 5)
        self.assertEqual(items, (19, 17, 15, 13, 11))

    def test_list_getitem_multiple_slice_pos_start_neg_step(self):
        @njit
        def foo():
            l = listobject.new_list(int32)
            for j in range(10, 20):
                l.append(j)
            n = l[4::-1]
            return len(n), (n[0], n[1], n[2], n[3], n[4])

        length, items = foo()
        self.assertEqual(length, 5)
        self.assertEqual(items, (14, 13, 12, 11, 10))

    def test_list_getitem_multiple_slice_neg_start_neg_step(self):
        @njit
        def foo():
            l = listobject.new_list(int32)
            for j in range(10, 20):
                l.append(j)
            n = l[-6::-1]
            return len(n), (n[0], n[1], n[2], n[3], n[4])

        length, items = foo()
        self.assertEqual(length, 5)
        self.assertEqual(items, (14, 13, 12, 11, 10))

    def test_list_getitem_multiple_slice_pos_stop_neg_step(self):
        @njit
        def foo():
            l = listobject.new_list(int32)
            for j in range(10, 20):
                l.append(j)
            n = l[:4:-1]
            return len(n), (n[0], n[1], n[2], n[3], n[4])

        length, items = foo()
        self.assertEqual(length, 5)
        self.assertEqual(items, (19, 18, 17, 16, 15))

    def test_list_getitem_multiple_slice_neg_stop_neg_step(self):
        @njit
        def foo():
            l = listobject.new_list(int32)
            for j in range(10, 20):
                l.append(j)
            n = l[:-6:-1]
            return len(n), (n[0], n[1], n[2], n[3], n[4])

        length, items = foo()
        self.assertEqual(length, 5)
        self.assertEqual(items, (19, 18, 17, 16, 15))

    def test_list_getitem_multiple_slice_pos_start_pos_stop_neg_step(self):
        @njit
        def foo():
            l = listobject.new_list(int32)
            for j in range(10, 20):
                l.append(j)
            n = l[8:3:-1]
            return len(n), (n[0], n[1], n[2], n[3], n[4])

        length, items = foo()
        self.assertEqual(length, 5)
        self.assertEqual(items, (18, 17, 16, 15, 14))

    def test_list_getitem_multiple_slice_neg_start_neg_stop_neg_step(self):
        @njit
        def foo():
            l = listobject.new_list(int32)
            for j in range(10, 20):
                l.append(j)
            n = l[-2:-7:-1]
            return len(n), (n[0], n[1], n[2], n[3], n[4])

        length, items = foo()
        self.assertEqual(length, 5)
        self.assertEqual(items, (18, 17, 16, 15, 14))

    def test_list_getitem_multiple_slice_start_out_of_range(self):
        @njit
        def foo():
            l = listobject.new_list(int32)
            for j in range(10, 20):
                l.append(j)
            n = l[10:]
            return len(n)

        self.assertEqual(foo(), 0)

    def test_list_getitem_multiple_slice_stop_zero(self):
        @njit
        def foo():
            l = listobject.new_list(int32)
            for j in range(10, 20):
                l.append(j)
            n = l[:0]
            return len(n)

        self.assertEqual(foo(), 0)

    def test_list_getitem_multiple_slice_zero_step_index_error(self):
        self.disable_leak_check()

        @njit
        def foo():
            l = listobject.new_list(int32)
            for j in range(10, 20):
                l.append(j)
            l[::0]

        with self.assertRaises(ValueError) as raises:
            foo()
        self.assertIn(
            "slice step cannot be zero",
            str(raises.exception),
        )


class TestSetitem(MemoryLeakMixin, TestCase):
    """Test list setitem. """

    def test_list_setitem_singleton(self):
        @njit
        def foo(n):
            l = listobject.new_list(int32)
            l.append(0)
            l[0] = n
            return l[0]

        for i in (0, 1, 2, 100):
            self.assertEqual(foo(i), i)

    def test_list_setitem_singleton_negative_index(self):
        @njit
        def foo(n):
            l = listobject.new_list(int32)
            l.append(0)
            l[0] = n
            return l[-1]

        for i in (0, 1, 2, 100):
            self.assertEqual(foo(i), i)

    def test_list_setitem_singleton_index_error(self):
        self.disable_leak_check()

        @njit
        def foo(i):
            l = listobject.new_list(int32)
            l.append(0)
            l[i] = 1

        with self.assertRaises(IndexError):
            foo(1)

        with self.assertRaises(IndexError):
            foo(-2)

    def test_list_setitem_multiple(self):

        @njit
        def foo(i, n):
            l = listobject.new_list(int32)
            for j in range(10, 20):
                l.append(j)
            l[i] = n
            return l[i]

        for i,n in zip(range(0,10), range(20,30)):
            self.assertEqual(foo(i, n), n)

    def test_list_setitem_multiple_index_error(self):
        self.disable_leak_check()

        @njit
        def foo(i):
            l = listobject.new_list(int32)
            for j in range(10, 20):
                l.append(j)
            l[i] = 0

        with self.assertRaises(IndexError):
            foo(10)

        with self.assertRaises(IndexError):
            foo(-11)

    def test_list_setitem_singleton_typing_error_on_index(self):
        self.disable_leak_check()

        @njit
        def foo(i):
            l = listobject.new_list(int32)
            l.append(0)
            # slice with a non-{integer,slice}
            l[i] = 1

        for i in "xyz", 1.0, 1j:
            with self.assertRaises(TypingError) as raises:
                foo(i)
            self.assertIn(
                "list indices must be integers or slices",
                str(raises.exception),
            )

    def test_list_setitem_singleton_typing_error_on_item(self):
        self.disable_leak_check()

        @njit
        def foo():
            l = listobject.new_list(int32)
            l.append(0)
            # assign a non-iterable to a slice
            l[:] = 1

        with self.assertRaises(TypingError) as raises:
            foo()
        self.assertIn(
            "can only assign an iterable when using a slice "
            "with assignment/setitem",
            str(raises.exception),
        )

    def test_list_setitem_integer_types_as_index(self):

        @njit
        def foo(i):
            l = listobject.new_list(int32)
            l.append(0)
            l[i] = 1
            return l[i]

        # try all signed integers and make sure they are cast
        for t in (types.signed_domain
                  ):
            self.assertEqual(foo((t(0))), 1)


class TestPop(MemoryLeakMixin, TestCase):
    """Test list pop. """

    def test_list_pop_singleton(self):
        @njit
        def foo():
            l = listobject.new_list(int32)
            l.append(0)
            return l.pop(), len(l)

        self.assertEqual(foo(), (0, 0))

    def test_list_pop_singleton_index(self):
        @njit
        def foo(i):
            l = listobject.new_list(int32)
            l.append(0)
            return l.pop(i), len(l)

        self.assertEqual(foo(0), (0, 0))
        self.assertEqual(foo(-1), (0, 0))

    def test_list_pop_multiple(self):
        @njit
        def foo():
            l = listobject.new_list(int32)
            for j in (10, 11, 12):
                l.append(j)
            return l.pop(), len(l)

        self.assertEqual(foo(), (12, 2))

    def test_list_pop_multiple_index(self):
        @njit
        def foo(i):
            l = listobject.new_list(int32)
            for j in (10, 11, 12):
                l.append(j)
            return l.pop(i), len(l)

        for i, n in ((0, 10), (1, 11), (2, 12)):
            self.assertEqual(foo(i), (n, 2))

        for i, n in ((-3, 10), (-2, 11), (-1, 12)):
            self.assertEqual(foo(i), (n, 2))

    def test_list_pop_integer_types_as_index(self):

        @njit
        def foo(i):
            l = listobject.new_list(int32)
            l.append(0)
            return l.pop(i)

        # try all signed integers and make sure they are cast
        for t in (types.signed_domain
                  ):
            self.assertEqual(foo((t(0))), 0)

    def test_list_pop_empty_index_error_no_index(self):
        self.disable_leak_check()

        @njit
        def foo():
            l = listobject.new_list(int32)
            l.pop()

        with self.assertRaises(IndexError) as raises:
            foo()
        self.assertIn(
            "pop from empty list",
            str(raises.exception),
        )

    def test_list_pop_empty_index_error_with_index(self):
        self.disable_leak_check()

        @njit
        def foo(i):
            l = listobject.new_list(int32)
            l.pop(i)

        with self.assertRaises(IndexError) as raises:
            foo(-1)
        self.assertIn(
            "pop from empty list",
            str(raises.exception),
        )

        with self.assertRaises(IndexError) as raises:
            foo(0)
        self.assertIn(
            "pop from empty list",
            str(raises.exception),
        )

        with self.assertRaises(IndexError) as raises:
            foo(1)
        self.assertIn(
            "pop from empty list",
            str(raises.exception),
        )

    def test_list_pop_mutiple_index_error_with_index(self):
        self.disable_leak_check()

        @njit
        def foo(i):
            l = listobject.new_list(int32)
            for j in (10, 11, 12):
                l.append(j)
            l.pop(i)

        with self.assertRaises(IndexError) as raises:
            foo(-4)
        self.assertIn(
            "list index out of range",
            str(raises.exception),
        )

        with self.assertRaises(IndexError) as raises:
            foo(3)
        self.assertIn(
            "list index out of range",
            str(raises.exception),
        )

    def test_list_pop_singleton_typing_error_on_index(self):
        self.disable_leak_check()

        @njit
        def foo(i):
            l = listobject.new_list(int32)
            l.append(0)
            # slice with a non-{integer,slice}
            return l.pop(i)

        for i in "xyz", 1.0, 1j:
            with self.assertRaises(TypingError) as raises:
                foo(i)
            self.assertIn(
                "argument for pop must be an integer",
                str(raises.exception),
            )


class TestListObjectDelitem(MemoryLeakMixin, TestCase):
    """Test list delitem.
    """

    def test_list_singleton_delitem_index(self):

        @njit
        def foo():
            l = listobject.new_list(int32)
            l.append(0)
            del l[0]
            return len(l)
        self.assertEqual(foo(), 0)

    def test_list_singleton_delitem_slice_defaults(self):

        @njit
        def foo():
            l = listobject.new_list(int32)
            l.append(0)
            del l[:]
            return len(l)
        self.assertEqual(foo(), 0)

    def test_list_singleton_delitem_slice_start(self):

        @njit
        def foo():
            l = listobject.new_list(int32)
            l.append(0)
            del l[0:]
            return len(l)
        self.assertEqual(foo(), 0)

    def test_list_singleton_delitem_slice_stop(self):

        @njit
        def foo():
            l = listobject.new_list(int32)
            l.append(0)
            del l[:1]
            return len(l)
        self.assertEqual(foo(), 0)

    def test_list_singleton_delitem_slice_start_stop(self):

        @njit
        def foo():
            l = listobject.new_list(int32)
            l.append(0)
            del l[0:1]
            return len(l)
        self.assertEqual(foo(), 0)

    def test_list_singleton_delitem_slice_start_step(self):

        @njit
        def foo():
            l = listobject.new_list(int32)
            l.append(0)
            del l[0::1]
            return len(l)
        self.assertEqual(foo(), 0)

    def test_list_singleton_delitem_slice_start_stop_step(self):

        @njit
        def foo():
            l = listobject.new_list(int32)
            l.append(0)
            del l[0:1:1]
            return len(l)
        self.assertEqual(foo(), 0)

    def test_list_multiple_delitem(self):

        @njit
        def foo():
            l = listobject.new_list(int32)
            for j in (10, 11, 12):
                l.append(j)
            del l[0]
            return len(l), l[0], l[1]
        self.assertEqual(foo(), (2, 11, 12))

    def test_list_multiple_delitem_slice(self):

        @njit
        def foo():
            l = listobject.new_list(int32)
            for j in (10, 11, 12):
                l.append(j)
            del l[:]
            return len(l)
        self.assertEqual(foo(), 0)

    def test_list_multiple_delitem_off_by_one(self):
        # this was exposing a nasty off-by-one error, leaving it in to detect
        # and regressions
        @njit
        def foo():
            l = listobject.new_list(int32)
            for j in range(10, 20):
                l.append(j)
            k = listobject.new_list(int32)
            for j in range(10, 20):
                k.append(j)
            # should be a no-op
            del l[-9:-20]
            return k == l
        self.assertTrue(foo())


class TestContains(MemoryLeakMixin, TestCase):
    """Test list contains. """

    def test_list_contains_empty(self):
        @njit
        def foo(i):
            l = listobject.new_list(int32)
            return i in l

        self.assertFalse(foo(0))
        self.assertFalse(foo(1))

    def test_list_contains_singleton(self):
        @njit
        def foo(i):
            l = listobject.new_list(int32)
            l.append(0)
            return i in l

        self.assertTrue(foo(0))
        self.assertFalse(foo(1))

    def test_list_contains_multiple(self):
        @njit
        def foo(i):
            l = listobject.new_list(int32)
            for j in range(10, 20):
                l.append(j)
            return i in l

        for i in range(10, 20):
            self.assertTrue(foo(i))

        for i in range(20, 30):
            self.assertFalse(foo(i))


class TestCount(MemoryLeakMixin, TestCase):
    """Test list count. """

    def test_list_count_empty(self):
        @njit
        def foo(i):
            l = listobject.new_list(int32)
            return l.count(i)

        self.assertEqual(foo(10), 0)

    def test_list_count_singleton(self):
        @njit
        def foo(i):
            l = listobject.new_list(int32)
            l.append(10)
            return l.count(i)

        self.assertEqual(foo(1), 0)
        self.assertEqual(foo(10), 1)

    def test_list_count_mutiple(self):
        @njit
        def foo(i):
            l = listobject.new_list(int32)
            for j in [11, 12, 12, 13, 13, 13]:
                l.append(j)
            return l.count(i)

        self.assertEqual(foo(10), 0)
        self.assertEqual(foo(11), 1)
        self.assertEqual(foo(12), 2)
        self.assertEqual(foo(13), 3)


class TestExtend(MemoryLeakMixin, TestCase):
    """Test list extend. """

    def test_list_extend_empty(self):
        @njit
        def foo(items):
            l = listobject.new_list(int32)
            l.extend(items)
            return len(l)

        self.assertEqual(foo((1,)), 1)
        self.assertEqual(foo((1,2)), 2)
        self.assertEqual(foo((1,2,3)), 3)

    def test_list_extend_typing_error_non_iterable(self):
        self.disable_leak_check()

        @njit
        def foo():
            l = listobject.new_list(int32)
            l.extend(1)

        with self.assertRaises(TypingError) as raises:
            foo()
        self.assertIn(
            "extend argument must be iterable",
            str(raises.exception),
        )


class TestInsert(MemoryLeakMixin, TestCase):
    """Test list insert. """

    def test_list_insert_empty(self):
        @njit
        def foo(i):
            l = listobject.new_list(int32)
            l.insert(i, 1)
            return len(l), l[0]

        for i in (-10, -5, -1, 0, 1, 4, 9):
            self.assertEqual(foo(i), (1, 1))

    def test_list_insert_singleton(self):
        @njit
        def foo(i):
            l = listobject.new_list(int32)
            l.append(0)
            l.insert(i, 1)
            return len(l), l[0], l[1]

        # insert before
        for i in (-10, -3, -2, -1, 0):
            self.assertEqual(foo(i), (2, 1, 0))

        # insert after
        for i in (1, 2, 3, 10):
            self.assertEqual(foo(i), (2, 0, 1))

    def test_list_insert_multiple(self):
        @njit
        def foo(i):
            l = listobject.new_list(int32)
            for j in range(10):
                l.append(0)
            l.insert(i, 1)
            return len(l), l[i]

        for i in (0, 4, 9):
            self.assertEqual(foo(i), (11, 1))

    def test_list_insert_multiple_before(self):
        @njit
        def foo(i):
            l = listobject.new_list(int32)
            for j in range(10):
                l.append(0)
            l.insert(i, 1)
            return len(l), l[0]

        for i in (-12, -11, -10, 0):
            self.assertEqual(foo(i), (11, 1))

    def test_list_insert_multiple_after(self):
        @njit
        def foo(i):
            l = listobject.new_list(int32)
            for j in range(10):
                l.append(0)
            l.insert(i, 1)
            return len(l), l[10]

        for i in (10, 11, 12):
            self.assertEqual(foo(i), (11, 1))

    def test_list_insert_typing_error(self):
        self.disable_leak_check()

        @njit
        def foo():
            l = listobject.new_list(int32)
            l.insert("a", 0)

        with self.assertRaises(TypingError) as raises:
            foo()
        self.assertIn(
            "list insert indices must be integers",
            str(raises.exception),
        )


class TestRemove(MemoryLeakMixin, TestCase):
    """Test list remove. """

    def test_list_remove_empty(self):
        self.disable_leak_check()

        @njit
        def foo():
            l = listobject.new_list(int32)
            l.remove(0)

        with self.assertRaises(ValueError):
            foo()

    def test_list_remove_singleton(self):
        @njit
        def foo():
            l = listobject.new_list(int32)
            l.append(0)
            l.remove(0)
            return len(l)

        self.assertEqual(foo(), 0)

    def test_list_remove_singleton_value_error(self):
        self.disable_leak_check()

        @njit
        def foo():
            l = listobject.new_list(int32)
            l.append(1)
            l.remove(0)

        with self.assertRaises(ValueError):
            foo()

    def test_list_remove_multiple(self):
        @njit
        def foo():
            l = listobject.new_list(int32)
            for j in range(10, 20):
                l.append(j)
            l.remove(13)
            l.remove(19)
            return len(l)

        self.assertEqual(foo(), 8)

    def test_list_remove_multiple_value_error(self):
        self.disable_leak_check()

        @njit
        def foo():
            l = listobject.new_list(int32)
            for j in range(10, 20):
                l.append(j)
            l.remove(23)

        with self.assertRaises(ValueError):
            foo()


class TestClear(MemoryLeakMixin, TestCase):
    """Test list clear. """

    def test_list_clear_empty(self):
        @njit
        def foo():
            l = listobject.new_list(int32)
            l.clear()
            return len(l)

        self.assertEqual(foo(), 0)

    def test_list_clear_singleton(self):
        @njit
        def foo():
            l = listobject.new_list(int32)
            l.append(0)
            l.clear()
            return len(l)

        self.assertEqual(foo(), 0)

    def test_list_clear_multiple(self):
        @njit
        def foo():
            l = listobject.new_list(int32)
            for j in range(10):
                l.append(0)
            l.clear()
            return len(l)
        self.assertEqual(foo(), 0)


class TestReverse(MemoryLeakMixin, TestCase):
    """Test list reverse. """

    def test_list_reverse_empty(self):
        @njit
        def foo():
            l = listobject.new_list(int32)
            l.reverse()
            return len(l)

        self.assertEqual(foo(), 0)

    def test_list_reverse_singleton(self):
        @njit
        def foo():
            l = listobject.new_list(int32)
            l.append(0)
            l.reverse()
            return len(l), l[0]

        self.assertEqual(foo(), (1, 0))

    def test_list_reverse_multiple(self):
        @njit
        def foo():
            l = listobject.new_list(int32)
            for j in range(10, 13):
                l.append(j)
            l.reverse()
            return len(l), l[0], l[1], l[2]
        self.assertEqual(foo(), (3, 12, 11, 10))


class TestCopy(MemoryLeakMixin, TestCase):
    """Test list copy. """

    def test_list_copy_empty(self):
        @njit
        def foo():
            l = listobject.new_list(int32)
            n = l.copy()
            return len(l), len(n)

        self.assertEqual(foo(), (0, 0))

    def test_list_copy_singleton(self):
        @njit
        def foo():
            l = listobject.new_list(int32)
            l.append(0)
            n = l.copy()
            return len(l), len(n), l[0], n[0]

        self.assertEqual(foo(), (1, 1, 0, 0))

    def test_list_copy_multiple(self):
        @njit
        def foo():
            l = listobject.new_list(int32)
            for j in range(10, 13):
                l.append(j)
            n = l.copy()
            return len(l), len(n), l[0], l[1], l[2], l[0], l[1], l[2]

        self.assertEqual(foo(), (3, 3, 10, 11, 12, 10, 11, 12))


class TestIndex(MemoryLeakMixin, TestCase):

    def test_index_singleton(self):
        @njit
        def foo():
            l = listobject.new_list(int32)
            l.append(1)
            return l.index(1)

        self.assertEqual(foo(), 0)

    def test_index_multiple(self):
        @njit
        def foo(i):
            l = listobject.new_list(int32)
            for j in range(10, 20):
                l.append(j)
            return l.index(i)

        for i,v in zip(range(10), range(10,20)):
            self.assertEqual(foo(v), i)

    def test_index_duplicate(self):
        @njit
        def foo():
            l = listobject.new_list(int32)
            for _ in range(10, 20):
                l.append(1)
            return l.index(1)

        self.assertEqual(foo(), 0)

    def test_index_duplicate_with_start(self):
        @njit
        def foo(start):
            l = listobject.new_list(int32)
            for _ in range(10, 20):
                l.append(1)
            return l.index(1, start)

        for i in range(10):
            self.assertEqual(foo(i), i)

    def test_index_singleton_value_error(self):
        self.disable_leak_check()

        @njit
        def foo():
            l = listobject.new_list(int32)
            l.append(0)
            return l.index(1)

        with self.assertRaises(ValueError) as raises:
            foo()
        self.assertIn(
            "item not in list",
            str(raises.exception),
        )

    def test_index_multiple_value_error(self):
        self.disable_leak_check()

        @njit
        def foo():
            l = listobject.new_list(int32)
            for j in range(10, 20):
                l.append(j)
            return l.index(23)

        with self.assertRaises(ValueError) as raises:
            foo()
        self.assertIn(
            "item not in list",
            str(raises.exception),
        )

    def test_index_multiple_value_error_start(self):
        self.disable_leak_check()

        @njit
        def foo(start):
            l = listobject.new_list(int32)
            for j in range(10, 20):
                l.append(j)
            return l.index(10, start)

        self.assertEqual(foo(0), 0)
        for i in range(1,10):
            with self.assertRaises(ValueError) as raises:
                foo(i)
            self.assertIn(
                "item not in list",
                str(raises.exception),
            )

    def test_index_multiple_value_error_end(self):
        self.disable_leak_check()

        @njit
        def foo(end):
            l = listobject.new_list(int32)
            for j in range(10, 20):
                l.append(j)
            return l.index(19, 0, end)

        self.assertEqual(foo(10), 9)
        for i in range(0,9):
            with self.assertRaises(ValueError) as raises:
                foo(i)
            self.assertIn(
                "item not in list",
                str(raises.exception),
            )

    def test_index_typing_error_start(self):
        self.disable_leak_check()

        @njit
        def foo():
            l = listobject.new_list(int32)
            l.append(0)
            return l.index(0, start="a")

        with self.assertRaises(TypingError) as raises:
            foo()
        self.assertIn(
            "start argument for index must be an integer",
            str(raises.exception),
        )

    def test_index_typing_error_end(self):
        self.disable_leak_check()

        @njit
        def foo():
            l = listobject.new_list(int32)
            l.append(0)
            return l.index(0, end="a")

        with self.assertRaises(TypingError) as raises:
            foo()
        self.assertIn(
            "end argument for index must be an integer",
            str(raises.exception),
        )


class TestEqualNotEqual(MemoryLeakMixin, TestCase):
    """Test list equal and not equal. """

    def test_list_empty_equal(self):
        @njit
        def foo():
            t = listobject.new_list(int32)
            o = listobject.new_list(int32)
            return t == o, t != o

        self.assertEqual(foo(), (True, False))

    def test_list_singleton_equal(self):
        @njit
        def foo():
            t = listobject.new_list(int32)
            t.append(0)
            o = listobject.new_list(int32)
            o.append(0)
            return t == o, t != o

        self.assertEqual(foo(), (True, False))

    def test_list_singleton_not_equal(self):
        @njit
        def foo():
            t = listobject.new_list(int32)
            t.append(0)
            o = listobject.new_list(int32)
            o.append(1)
            return t == o, t != o

        self.assertEqual(foo(), (False, True))

    def test_list_length_mismatch(self):
        @njit
        def foo():
            t = listobject.new_list(int32)
            t.append(0)
            o = listobject.new_list(int32)
            return t == o, t != o

        self.assertEqual(foo(), (False, True))

    def test_list_multiple_equal(self):
        @njit
        def foo():
            t = listobject.new_list(int32)
            o = listobject.new_list(int32)
            for i in range(10):
                t.append(i)
                o.append(i)
            return t == o, t != o

        self.assertEqual(foo(), (True, False))

    def test_list_multiple_not_equal(self):
        @njit
        def foo():
            t = listobject.new_list(int32)
            o = listobject.new_list(int32)
            for i in range(10):
                t.append(i)
                o.append(i)
            o[-1] = 42
            return t == o, t != o

        self.assertEqual(foo(), (False, True))


class TestIter(MemoryLeakMixin, TestCase):
    """Test list iter. """

    def test_list_iter(self):
        @njit
        def foo(items):
            l = listobject.new_list(int32)
            l.extend(items)
            # use a simple sum to check this w/o having to return a list
            r = 0
            for j in l:
                r += j
            return r

        items = (1, 2, 3, 4)

        self.assertEqual(
            foo(items),
            sum(items)
        )

    def test_list_iter_self_mutation(self):
        self.disable_leak_check()

        @njit
        def foo():
            l = listobject.new_list(int32)
            l.extend((1, 2, 3, 4))
            for i in l:
                l.append(i)

        with self.assertRaises(RuntimeError) as raises:
            foo()
        self.assertIn(
            'list was mutated during iteration'.format(**locals()),
            str(raises.exception),
        )


class TestStringItem(MemoryLeakMixin, TestCase):
    """Test list can take strings as items. """

    def test_string_item(self):
        @njit
        def foo():
            l = listobject.new_list(types.unicode_type)
            l.append('a')
            l.append('b')
            l.append('c')
            l.append('d')
            return l[0], l[1], l[2], l[3]

        items = foo()
        self.assertEqual(['a', 'b', 'c', 'd'], list(items))


class TestItemCasting(TestCase):

    @njit
    def foo(fromty, toty):
        l = listobject.new_list(toty)
        l.append(fromty(0))

    def check_good(self, fromty, toty):
        TestItemCasting.foo(fromty, toty)

    def check_bad(self, fromty, toty):
        with self.assertRaises(TypingError) as raises:
            TestItemCasting.foo(fromty, toty)
        self.assertIn(
            'cannot safely cast {fromty} to {toty}'.format(**locals()),
            str(raises.exception),
        )

    def test_cast_int_to(self):
        self.check_good(types.int32, types.float32)
        self.check_good(types.int32, types.float64)
        self.check_good(types.int32, types.complex128)
        self.check_good(types.int64, types.complex128)
        self.check_bad(types.int32, types.complex64)
        self.check_good(types.int8, types.complex64)

    def test_cast_float_to(self):
        self.check_good(types.float32, types.float64)
        self.check_good(types.float32, types.complex64)
        self.check_good(types.float64, types.complex128)

    def test_cast_bool_to(self):
        self.check_good(types.boolean, types.int32)
        self.check_good(types.boolean, types.float64)
        self.check_good(types.boolean, types.complex128)

    def test_cast_fail_unicode_int(self):

        @njit
        def foo():
            l = listobject.new_list(int32)
            l.append("xyz")

        with self.assertRaises(TypingError) as raises:
            foo()
        self.assertIn(
            'cannot safely cast unicode_type to int32',
            str(raises.exception),
        )

    def test_cast_fail_int_unicode(self):

        @njit
        def foo():
            l = listobject.new_list(types.unicode_type)
            l.append(int32(0))

        with self.assertRaises(TypingError) as raises:
            foo()
        self.assertIn(
            'Cannot cast int32 to unicode_type',
            str(raises.exception),
        )


@register_jitable
def make_test_list():
    l = listobject.new_list(int32)
    l.append(int32(1))
    return l


class TestImmutable(MemoryLeakMixin, TestCase):

    def test_is_immutable(self):
        @njit
        def foo():
            l = make_test_list()
            return l._is_mutable()
        self.assertTrue(foo())

    def test_make_immutable_is_immutable(self):
        @njit
        def foo():
            l = make_test_list()
            l._make_immutable()
            return l._is_mutable()
        self.assertFalse(foo())

    def test_length_still_works_when_immutable(self):
        @njit
        def foo():
            l = make_test_list()
            l._make_immutable()
            return len(l),l._is_mutable()
        length, mutable = foo()
        self.assertEqual(length, 1)
        self.assertFalse(mutable)

    def test_getitem_still_works_when_immutable(self):
        @njit
        def foo():
            l = make_test_list()
            l._make_immutable()
            return l[0], l._is_mutable()
        test_item, mutable = foo()
        self.assertEqual(test_item, 1)
        self.assertFalse(mutable)

    def test_append_fails(self):
        self.disable_leak_check()

        @njit
        def foo():
            l = make_test_list()
            l._make_immutable()
            l.append(int32(1))
        with self.assertRaises(ValueError) as raises:
            foo()
        self.assertIn(
            'list is immutable',
            str(raises.exception),
        )

    def test_mutation_fails(self):
        """ Test that any attempt to mutate an immutable typed list fails. """
        self.disable_leak_check()

        def generate_function(line):
            context = {}
            exec(dedent("""
                from numba.typed import listobject
                from numba import int32
                def bar():
                    lst = listobject.new_list(int32)
                    lst.append(int32(1))
                    lst._make_immutable()
                    zero = int32(0)
                    {}
                """.format(line)), context)
            return njit(context["bar"])
        for line in ("lst.append(zero)",
                     "lst[0] = zero",
                     "lst.pop()",
                     "del lst[0]",
                     "lst.extend((zero,))",
                     "lst.insert(0, zero)",
                     "lst.clear()",
                     "lst.reverse()",
                     "lst.sort()",
                     ):
            foo = generate_function(line)
            with self.assertRaises(ValueError) as raises:
                foo()
            self.assertIn(
                "list is immutable",
                str(raises.exception),
            )
