# Example of an undefined singleton, usable with eval() functions to limit
# possibility of NameError exceptions.
#
# Use with a namespace decorator like :
#
# class NoNameErrorNamespace:
#     def __init__(self, ns):
#         self.ns = ns
#
#     def __getitem__(self, name):
#         try:
#             return self.ns[name]
#         except KeyError:
#             return Undefined()
#
#     def __setitem__(self, name, value):
#         self.ns[name] = value
#
# eval('x - 1')


class Undefined:
    def __str__(self):
        return 'Undefined'

    def __getattr__(self, name):
        return self

    def __neq__(self, other):
        return True

    self = lambda self, *args, **kwargs: self
    true = lambda *args: True
    false = lambda *args: False

    __eq__ = self
    __lt__ = false
    __gt__ = false
    __le__ = false
    __ge__ = false

    # Arithmetic operators
    for op in ['__add__', '__sub__', '__mul__', '__matmul__', '__truediv__',
               '__floordiv__', '__mod__', '__divmod__', '__pow__',
               '__lshift__', '__rshift__', '__and__', '__xor__', '__or__']:
        locals()[op] = self
        locals()[op.replace('__', '__r', 1)] = self
        locals()[op.replace('__', '__i', 1)] = self

    __call__ = self
    __getitem__ = self
    __bool__ = false
    __abs__ = self
    __neg__ = self
    __pos__ = self
    __invert__ = self
    __contains__ = self
    __reversed__ = self
    __length_hint__ = lambda: 0
    __hash__ = lambda self: id(self)

    __instance = None
    def __new__(cls, *args, **kwargs):
        if cls.__instance is None:
            cls.__instance = super().__new__(cls)
        return cls.__instance


import operator
import inspect
import pytest


def test():
    assert Undefined().__lt__(1) is False

    assert Undefined() is Undefined()
    assert Undefined().x is Undefined()
    assert Undefined()['a'] is Undefined()
    assert Undefined()() is Undefined()
    assert Undefined().x('a', 1, x=3) is Undefined()
    assert not Undefined()
    assert not bool(Undefined())
    assert (Undefined() - 1) is Undefined()
    assert (1 - Undefined()) is Undefined()


ops = [op for key, op in operator.__dict__.items() if callable(op) and key.startswith('__')]
@pytest.mark.parametrize('obj', [1, '', [], (), {}, dict()], ids=['integer', 'string', 'list', 'tuple', 'set', 'dict'])
@pytest.mark.parametrize('op', ops, ids=[op.__name__ for op in ops])
def test_op(op, obj):
    if isinstance(op, type):
        pytest.skip()
    name = op.__name__.strip('_').strip('0')
    if name in ['index', 'concat', 'countOf', 'delitem', 'indexOf', 'setitem', 'attrgetter', 'iconcat']:
        pytest.skip()
    args_len = len([p for p in inspect.signature(op).parameters.values() if p.default is p.empty])
    if args_len == 0:
        pytest.skip('no arg')
    result = Undefined()
    if name in ['lt', 'gt', 'le', 'ge', 'truth', 'is', 'contains']:
        result = False
    if name in ['ne', 'not', 'is_not']:
        result = True
    if name in ['length_hint']:
        result = 0
    result_reversed = result
    if name in ['imod', 'mod'] and obj == '':
        result_reversed = ''
    if args_len == 1:
        assert op(Undefined()) is result, f'failed with {op}'
    elif args_len == 2:
        assert op(Undefined(), obj) is result, f'failed with {op}'
        if name not in ['contains', 'getitem'] or (name == 'ctonains' and obj != '' and hasattr(obj, '__contains__')) or (name == 'getitem' and hasattr(obj, '__getitem__') and obj != '' and obj != [] and obj != () and obj != {}):
            assert op(obj, Undefined()) is result_reversed, f'failed with reversed {op}'
    else:
        raise NotImplementedError(f'{op} {args_len}')
