import sys
import pytest
from panda3d import core
from contextlib import contextmanager
import collections.abc as collections_abc


@contextmanager
def constant_refcount(var):
    """ with block that checks that the Python refcount remains the same. """
    rc = sys.getrefcount(var)
    yield
    assert sys.getrefcount(var) == rc


def test_property():
    # This is a property defined by MAKE_PROPERTY.
    np = core.PandaNode("")

    # Getter
    transform = np.get_transform()
    assert transform == np.transform

    # Setter
    new_transform = transform.set_pos((1, 0, 0))
    np.transform = new_transform
    assert np.transform == new_transform

    # Invalid assignments
    with pytest.raises(TypeError):
        np.transform = None
    with pytest.raises(TypeError):
        np.transform = "nonsense"
    with pytest.raises(TypeError):
        del np.transform


def test_property2():
    # This is a property defined by MAKE_PROPERTY2, that can be None.
    mat = core.Material()

    mat.ambient = (1, 0, 0, 1)
    assert mat.ambient == (1, 0, 0, 1)

    mat.ambient = None
    assert mat.ambient is None

    with pytest.raises(TypeError):
        mat.ambient = "nonsense"
    with pytest.raises(TypeError):
        del mat.ambient


# The next tests are for MAKE_SEQ_PROPERTY.
def seq_property(*items):
    """ Returns a sequence property initialized with the given items. """

    # It doesn't matter which property we use; I just happened to pick
    # CollisionNode.solids.
    cn = core.CollisionNode("")
    append = cn.add_solid
    for item in items:
        append(item)
    assert len(cn.solids) == len(items)
    return cn.solids

# Arbitrary items we can use as items for the above seq property.
item_a = core.CollisionSphere((0, 0, 0), 1)
item_b = core.CollisionSphere((0, 0, 0), 2)
item_c = core.CollisionSphere((0, 0, 0), 3)


def test_seq_property_abc():
    prop = seq_property()
    assert isinstance(prop, collections_abc.Container)
    assert isinstance(prop, collections_abc.Sized)
    assert isinstance(prop, collections_abc.Iterable)
    assert isinstance(prop, collections_abc.MutableSequence)
    assert isinstance(prop, collections_abc.Sequence)


def test_seq_property_empty():
    prop = seq_property()
    assert not prop
    assert len(prop) == 0

    with pytest.raises(IndexError):
        prop[0]
    with pytest.raises(IndexError):
        prop[-1]


def test_seq_property_iter():
    prop = seq_property(item_a, item_b, item_b)
    assert prop
    assert len(prop) == 3

    assert tuple(prop) == (item_a, item_b, item_b)
    assert item_a in prop
    assert item_c not in prop
    assert None not in prop


def test_seq_property_reversed():
    prop = seq_property(item_a, item_b, item_b)
    assert tuple(reversed(prop)) == tuple(reversed(tuple(prop)))


def test_seq_property_getitem():
    prop = seq_property(item_a, item_b, item_b)

    assert prop[0] == item_a
    assert prop[1] == item_b
    assert prop[2] == item_b

    # Reverse index
    assert prop[-1] == item_b
    assert prop[-2] == item_b
    assert prop[-3] == item_a

    # Out of bounds access
    with pytest.raises(IndexError):
        prop[-4]
    with pytest.raises(IndexError):
        prop[3]
    with pytest.raises(IndexError):
        prop[2**63]

    # Invalid index
    with pytest.raises(TypeError):
        prop[None]
    with pytest.raises(TypeError):
        prop[item_a]
    with pytest.raises(TypeError):
        prop["nonsense"]

    # Reference count check
    i = 1
    with constant_refcount(i):
        prop[i]

    # Make sure it preserves refcount of invalid indices
    i = "nonsense195641"
    with constant_refcount(i):
        try:
            prop[i]
        except TypeError:
            pass


def test_seq_property_setitem():
    prop = seq_property(item_c, item_c, item_c)

    prop[0] = item_a
    prop[1] = item_b
    assert tuple(prop) == (item_a, item_b, item_c)

    # Refcount of key and value stays the same?
    i = 0
    with constant_refcount(i):
        with constant_refcount(item_a):
            prop[0] = item_a

    # Reverse index
    prop[-1] = item_a
    prop[-2] = item_b
    prop[-3] = item_c
    assert tuple(prop) == (item_c, item_b, item_a)

    # Out of bounds access
    with pytest.raises(IndexError):
        prop[-4] = item_c
    with pytest.raises(IndexError):
        prop[3] = item_c
    with pytest.raises(IndexError):
        prop[2**63] = item_c

    # Invalid index
    with pytest.raises(TypeError):
        prop[None] = item_c
    with pytest.raises(TypeError):
        prop[item_a] = item_c
    with pytest.raises(TypeError):
        prop["nonsense"] = item_c

    # Invalid type
    with pytest.raises(TypeError):
        prop[0] = None
    with pytest.raises(TypeError):
        prop[0] = "nonsense"


def test_seq_property_delitem():
    prop = seq_property(item_a, item_b, item_c)

    # Out of bounds
    with pytest.raises(IndexError):
        prop[3]
    with pytest.raises(IndexError):
        prop[-4]

    # Positive index
    del prop[0]
    assert tuple(prop) == (item_b, item_c)

    # Negative index
    del prop[-2]
    assert tuple(prop) == (item_c,)

    # Invalid index
    with pytest.raises(TypeError):
        del prop[None]


def test_seq_property_index():
    prop = seq_property(item_a, item_b, item_b)

    assert prop.index(item_a) == 0
    assert prop.index(item_b) == 1

    with pytest.raises(ValueError):
        prop.index(item_c)
    with pytest.raises(ValueError):
        prop.index(None)
    with pytest.raises(ValueError):
        prop.index("nonsense")

    # Refcount is properly decreased
    with constant_refcount(item_b):
        prop.index(item_b)
    with constant_refcount(item_c):
        try:
            prop.index(item_c)
        except ValueError:
            pass

    nonsense = "nonsense854371"
    with constant_refcount(nonsense):
        try:
            prop.index(nonsense)
        except ValueError:
            pass


def test_seq_property_count():
    prop = seq_property(item_a, item_b, item_b)

    prop.count(item_a) == 1
    prop.count(item_b) == 2
    prop.count(item_c) == 0
    prop.count(None) == 0
    prop.count(("nonsense", -3.5)) == 0

    # Refcount does not leak
    with constant_refcount(item_b):
        prop.count(item_b)

    nonsense = "nonsense219449"
    with constant_refcount(nonsense):
        prop.count(nonsense)


def test_seq_property_clear():
    prop = seq_property(item_a, item_b, item_b)
    prop.clear()

    assert not prop
    assert len(prop) == 0
    assert tuple(prop) == ()


def test_seq_property_pop():
    prop = seq_property(item_a, item_b, item_c, item_b)

    # Test out of bounds pop
    with pytest.raises(IndexError):
        prop.pop(4)
    with pytest.raises(IndexError):
        prop.pop(-5)

    assert prop.pop(1) == item_b
    assert prop.pop(-1) == item_b
    assert prop.pop() == item_c
    assert prop.pop(0) == item_a

    # Wrong args
    with pytest.raises(TypeError):
        prop.pop(0, 0)

    # Pop on empty sequence
    with pytest.raises(IndexError):
        prop.pop()
    with pytest.raises(IndexError):
        prop.pop(0)
    with pytest.raises(IndexError):
        prop.pop(-1)


def test_seq_property_remove():
    prop = seq_property(item_a, item_b, item_c, item_b)

    with constant_refcount(item_b):
        prop.remove(item_b)

    assert tuple(prop) == (item_a, item_c, item_b)

    prop.remove(item_b)
    assert tuple(prop) == (item_a, item_c)

    with pytest.raises(ValueError):
        prop.remove(item_b)
    with pytest.raises(ValueError):
        prop.remove(None)
    with pytest.raises(ValueError):
        prop.remove("nonsense")


def test_seq_property_append():
    prop = seq_property(item_a, item_b)

    with constant_refcount(item_c):
        prop.append(item_c)

    assert tuple(prop) == (item_a, item_b, item_c)

    with pytest.raises(TypeError):
        prop.append(None)
    with pytest.raises(TypeError):
        prop.append("nonsense")


def test_seq_property_insert():
    # Adding at the beginning
    prop = seq_property(item_a, item_a, item_a)
    with constant_refcount(item_b):
        prop.insert(0, item_b)

    assert tuple(prop) == (item_b, item_a, item_a, item_a)

    # Adding in the middle
    prop = seq_property(item_a, item_a, item_a)
    with constant_refcount(item_b):
        prop.insert(2, item_b)

    assert tuple(prop) == (item_a, item_a, item_b, item_a)

    # Adding at the end
    prop = seq_property(item_a, item_a, item_a)
    with constant_refcount(item_b):
        prop.insert(len(prop), item_b)

    assert tuple(prop) == (item_a, item_a, item_a, item_b)

    # Adding with negative index
    prop = seq_property(item_a, item_a, item_a)
    with constant_refcount(item_b):
        prop.insert(-2, item_b)

    assert tuple(prop) == (item_a, item_b, item_a, item_a)

    # Adding at the end with overflowing index
    prop = seq_property(item_a, item_a, item_a)
    with constant_refcount(item_b):
        prop.insert(2345, item_b)

    assert tuple(prop) == (item_a, item_a, item_a, item_b)

    # Adding at the beginning with negative overflowing index
    prop = seq_property(item_a, item_a, item_a)
    with constant_refcount(item_b):
        prop.insert(-2345, item_b)

    assert tuple(prop) == (item_b, item_a, item_a, item_a)


def test_seq_property_extend():
    prop = seq_property(item_a)

    with constant_refcount(item_b):
        prop.extend((item_b, item_c))

    assert tuple(prop) == (item_a, item_b, item_c)

    with pytest.raises(TypeError):
        prop.extend(None)
    with pytest.raises(TypeError):
        prop.extend("nonsense")
    with pytest.raises(TypeError):
        prop.extend(item_a)
    with pytest.raises(TypeError):
        prop.extend(item_a, item_b)
    with pytest.raises(TypeError):
        prop.extend()
    with pytest.raises(TypeError):
        prop.extend((item_a, None))
    with pytest.raises(TypeError):
        prop.extend(["nonsense"])


# The next tests are for MAKE_MAP_PROPERTY.
def map_property(**items):
    """ Returns a mapping property initialized with the given values. """

    # It doesn't matter which property we use; I just happened to pick
    # NodePath.tags.
    np = core.NodePath("")
    for k, v in items.items():
        np.set_tag(k, v)
    return np.tags


def test_map_property_abc():
    prop = map_property()
    assert isinstance(prop, collections_abc.Container)
    assert isinstance(prop, collections_abc.Sized)
    assert isinstance(prop, collections_abc.Iterable)
    assert isinstance(prop, collections_abc.MutableMapping)
    assert isinstance(prop, collections_abc.Mapping)


def test_map_property_empty():
    prop = map_property()
    assert not prop
    assert len(prop) == 0

    with pytest.raises(KeyError):
        prop.popitem()

    with pytest.raises(KeyError):
        prop['nonsense']


def test_map_property_getitem():
    key = 'key162732'
    value = 'value162732'
    prop = map_property(**{key: value})

    with constant_refcount(key):
        with constant_refcount(value):
            assert prop[key] == value

    with pytest.raises(KeyError):
        prop['nonsense']
    with pytest.raises(TypeError):
        prop[None]


def test_map_property_setitem():
    key = 'key559075'
    value = 'value559075'
    prop = map_property()

    # Setting new key
    with constant_refcount(key):
        with constant_refcount(value):
            prop[key] = value

    assert prop[key] == value

    # Setting existing key
    with constant_refcount(key):
        with constant_refcount(value):
            prop[key] = value

    assert prop[key] == value

    # Unknown key
    with pytest.raises(TypeError):
        prop[None] = value

    # Unknown value
    with pytest.raises(TypeError):
        prop[key] = None


def test_map_property_delitem():
    key = 'key731691'
    value = 'value731691'
    prop = map_property(**{key: value})

    with constant_refcount(key):
        with constant_refcount(value):
            del prop[key]

    with pytest.raises(KeyError):
        assert prop[key]

    # Nonexistent key
    with pytest.raises(KeyError):
        del prop['nonsense']

    # Invalid type key
    with pytest.raises(TypeError):
        del prop[None]


def test_map_property_contains():
    prop = map_property(key='value')

    assert 'key' in prop
    assert None not in prop
    assert 'value' not in prop


def test_map_property_get():
    key = 'key861801'
    value = 'value861801'
    prop = map_property(**{key: value})

    default = 'default861801'
    with constant_refcount(key):
        with constant_refcount(default):
            assert prop.get(key) == value

    with constant_refcount(key):
        with constant_refcount(default):
            assert prop.get(key, default) == value

    assert prop.get('unknown') is None

    with constant_refcount(default):
        assert prop.get('unknown', default) == default


def test_map_property_pop():
    key = 'key'
    value = 'value'
    prop = map_property(**{key: value})

    assert prop.pop('nonsense', None) is None
    assert prop.pop('nonsense', 'default') == 'default'

    assert prop.pop('key', 'default') == 'value'
    assert 'key' not in prop


def test_map_property_popitem():
    key = 'key'
    value = 'value'
    prop = map_property(**{key: value})

    assert prop.popitem() == (key, value)

    with pytest.raises(KeyError):
        assert prop.popitem()


def test_map_property_clear():
    prop = map_property(key='value', key2='value2')

    prop.clear()
    assert len(prop) == 0


def test_map_property_setdefault():
    prop = map_property(key='value')

    # Don't change value of key that already exists
    prop.setdefault('key', 'value2')
    assert prop['key'] == 'value'

    # Change values of nonexistent key
    prop.setdefault('key2', 'value2')
    assert prop['key2'] == 'value2'

    # These should error because you can't set None on this property
    with pytest.raises(TypeError):
        prop.setdefault('key3', None)
    with pytest.raises(TypeError):
        prop.setdefault('key3')


def test_map_property_update():
    prop = map_property()

    # Empty update
    prop.update()

    # Passing in dictionary
    prop.update({'key': 'value'})

    # Passing in keywords
    prop.update(key2='value2')

    # Don't pass in both!
    with pytest.raises(TypeError):
        prop.update({}, k='v')

    assert prop['key'] == 'value'
    assert prop['key2'] == 'value2'


def test_map_property_keys():
    prop = map_property(key='value', key2='value2')

    assert isinstance(prop.keys(), collections_abc.MappingView)
    assert frozenset(prop.keys()) == frozenset(('key', 'key2'))


def test_map_property_values():
    prop = map_property(key='value', key2='value2')

    assert isinstance(prop.values(), collections_abc.ValuesView)
    assert frozenset(prop.values()) == frozenset(('value', 'value2'))


def test_map_property_items():
    prop = map_property(key='value', key2='value2')

    assert isinstance(prop.items(), collections_abc.MappingView)
    assert frozenset(prop.items()) == frozenset((('key', 'value'), ('key2', 'value2')))
