"""Unit tests for zero-argument super() & related machinery."""

import textwrap
import unittest
from unittest.mock import patch
from test.support import import_helper


ADAPTIVE_WARMUP_DELAY = 2


class A:
    def f(self):
        return 'A'
    @classmethod
    def cm(cls):
        return (cls, 'A')

class B(A):
    def f(self):
        return super().f() + 'B'
    @classmethod
    def cm(cls):
        return (cls, super().cm(), 'B')

class C(A):
    def f(self):
        return super().f() + 'C'
    @classmethod
    def cm(cls):
        return (cls, super().cm(), 'C')

class D(C, B):
    def f(self):
        return super().f() + 'D'
    def cm(cls):
        return (cls, super().cm(), 'D')

class E(D):
    pass

class F(E):
    f = E.f

class G(A):
    pass


class TestSuper(unittest.TestCase):

    def tearDown(self):
        # This fixes the damage that test_various___class___pathologies does.
        nonlocal __class__
        __class__ = TestSuper

    def test_basics_working(self):
        self.assertEqual(D().f(), 'ABCD')

    def test_class_getattr_working(self):
        self.assertEqual(D.f(D()), 'ABCD')

    def test_subclass_no_override_working(self):
        self.assertEqual(E().f(), 'ABCD')
        self.assertEqual(E.f(E()), 'ABCD')

    def test_unbound_method_transfer_working(self):
        self.assertEqual(F().f(), 'ABCD')
        self.assertEqual(F.f(F()), 'ABCD')

    def test_class_methods_still_working(self):
        self.assertEqual(A.cm(), (A, 'A'))
        self.assertEqual(A().cm(), (A, 'A'))
        self.assertEqual(G.cm(), (G, 'A'))
        self.assertEqual(G().cm(), (G, 'A'))

    def test_super_in_class_methods_working(self):
        d = D()
        self.assertEqual(d.cm(), (d, (D, (D, (D, 'A'), 'B'), 'C'), 'D'))
        e = E()
        self.assertEqual(e.cm(), (e, (E, (E, (E, 'A'), 'B'), 'C'), 'D'))

    def test_super_with_closure(self):
        # Issue4360: super() did not work in a function that
        # contains a closure
        class E(A):
            def f(self):
                def nested():
                    self
                return super().f() + 'E'

        self.assertEqual(E().f(), 'AE')

    def test_various___class___pathologies(self):
        # See issue #12370
        class X(A):
            def f(self):
                return super().f()
            __class__ = 413
        x = X()
        self.assertEqual(x.f(), 'A')
        self.assertEqual(x.__class__, 413)
        class X:
            x = __class__
            def f():
                __class__
        self.assertIs(X.x, type(self))
        with self.assertRaises(NameError) as e:
            exec("""class X:
                __class__
                def f():
                    __class__""", globals(), {})
        self.assertIs(type(e.exception), NameError) # Not UnboundLocalError
        class X:
            global __class__
            __class__ = 42
            def f():
                __class__
        self.assertEqual(globals()["__class__"], 42)
        del globals()["__class__"]
        self.assertNotIn("__class__", X.__dict__)
        class X:
            nonlocal __class__
            __class__ = 42
            def f():
                __class__
        self.assertEqual(__class__, 42)

    def test___class___instancemethod(self):
        # See issue #14857
        class X:
            def f(self):
                return __class__
        self.assertIs(X().f(), X)

    def test___class___classmethod(self):
        # See issue #14857
        class X:
            @classmethod
            def f(cls):
                return __class__
        self.assertIs(X.f(), X)

    def test___class___staticmethod(self):
        # See issue #14857
        class X:
            @staticmethod
            def f():
                return __class__
        self.assertIs(X.f(), X)

    def test___class___new(self):
        # See issue #23722
        # Ensure zero-arg super() works as soon as type.__new__() is completed
        test_class = None

        class Meta(type):
            def __new__(cls, name, bases, namespace):
                nonlocal test_class
                self = super().__new__(cls, name, bases, namespace)
                test_class = self.f()
                return self

        class A(metaclass=Meta):
            @staticmethod
            def f():
                return __class__

        self.assertIs(test_class, A)

    def test___class___delayed(self):
        # See issue #23722
        test_namespace = None

        class Meta(type):
            def __new__(cls, name, bases, namespace):
                nonlocal test_namespace
                test_namespace = namespace
                return None

        class A(metaclass=Meta):
            @staticmethod
            def f():
                return __class__

        self.assertIs(A, None)

        B = type("B", (), test_namespace)
        self.assertIs(B.f(), B)

    def test___class___mro(self):
        # See issue #23722
        test_class = None

        class Meta(type):
            def mro(self):
                # self.f() doesn't work yet...
                self.__dict__["f"]()
                return super().mro()

        class A(metaclass=Meta):
            def f():
                nonlocal test_class
                test_class = __class__

        self.assertIs(test_class, A)

    def test___classcell___expected_behaviour(self):
        # See issue #23722
        class Meta(type):
            def __new__(cls, name, bases, namespace):
                nonlocal namespace_snapshot
                namespace_snapshot = namespace.copy()
                return super().__new__(cls, name, bases, namespace)

        # __classcell__ is injected into the class namespace by the compiler
        # when at least one method needs it, and should be omitted otherwise
        namespace_snapshot = None
        class WithoutClassRef(metaclass=Meta):
            pass
        self.assertNotIn("__classcell__", namespace_snapshot)

        # With zero-arg super() or an explicit __class__ reference,
        # __classcell__ is the exact cell reference to be populated by
        # type.__new__
        namespace_snapshot = None
        class WithClassRef(metaclass=Meta):
            def f(self):
                return __class__

        class_cell = namespace_snapshot["__classcell__"]
        method_closure = WithClassRef.f.__closure__
        self.assertEqual(len(method_closure), 1)
        self.assertIs(class_cell, method_closure[0])
        # Ensure the cell reference *doesn't* get turned into an attribute
        with self.assertRaises(AttributeError):
            WithClassRef.__classcell__

    def test___classcell___missing(self):
        # See issue #23722
        # Some metaclasses may not pass the original namespace to type.__new__
        # We test that case here by forcibly deleting __classcell__
        class Meta(type):
            def __new__(cls, name, bases, namespace):
                namespace.pop('__classcell__', None)
                return super().__new__(cls, name, bases, namespace)

        # The default case should continue to work without any errors
        class WithoutClassRef(metaclass=Meta):
            pass

        # With zero-arg super() or an explicit __class__ reference, we expect
        # __build_class__ to raise a RuntimeError complaining that
        # __class__ was not set, and asking if __classcell__ was propagated
        # to type.__new__.
        expected_error = '__class__ not set.*__classcell__ propagated'
        with self.assertRaisesRegex(RuntimeError, expected_error):
            class WithClassRef(metaclass=Meta):
                def f(self):
                    return __class__

    def test___classcell___overwrite(self):
        # See issue #23722
        # Overwriting __classcell__ with nonsense is explicitly prohibited
        class Meta(type):
            def __new__(cls, name, bases, namespace, cell):
                namespace['__classcell__'] = cell
                return super().__new__(cls, name, bases, namespace)

        for bad_cell in (None, 0, "", object()):
            with self.subTest(bad_cell=bad_cell):
                with self.assertRaises(TypeError):
                    class A(metaclass=Meta, cell=bad_cell):
                        pass

    def test___classcell___wrong_cell(self):
        # See issue #23722
        # Pointing the cell reference at the wrong class is also prohibited
        class Meta(type):
            def __new__(cls, name, bases, namespace):
                cls = super().__new__(cls, name, bases, namespace)
                B = type("B", (), namespace)
                return cls

        with self.assertRaises(TypeError):
            class A(metaclass=Meta):
                def f(self):
                    return __class__

    def test_obscure_super_errors(self):
        def f():
            super()
        with self.assertRaisesRegex(RuntimeError, r"no arguments"):
            f()

        class C:
            def f():
                super()
        with self.assertRaisesRegex(RuntimeError, r"no arguments"):
            C.f()

        def f(x):
            del x
            super()
        with self.assertRaisesRegex(RuntimeError, r"arg\[0\] deleted"):
            f(None)

        class X:
            def f(x):
                nonlocal __class__
                del __class__
                super()
        with self.assertRaisesRegex(RuntimeError, r"empty __class__ cell"):
            X().f()

    def test_cell_as_self(self):
        class X:
            def meth(self):
                super()

        def f():
            k = X()
            def g():
                return k
            return g
        c = f().__closure__[0]
        self.assertRaises(TypeError, X.meth, c)

    def test_super_init_leaks(self):
        # Issue #26718: super.__init__ leaked memory if called multiple times.
        # This will be caught by regrtest.py -R if this leak.
        # NOTE: Despite the use in the test a direct call of super.__init__
        # is not endorsed.
        sp = super(float, 1.0)
        for i in range(1000):
            super.__init__(sp, int, i)

    def test_super_argcount(self):
        with self.assertRaisesRegex(TypeError, "expected at most"):
            super(int, int, int)

    def test_super_argtype(self):
        with self.assertRaisesRegex(TypeError, "argument 1 must be a type"):
            super(1, int)

    def test_shadowed_global(self):
        source = textwrap.dedent(
            """
            class super:
                msg = "truly super"

            class C:
                def method(self):
                    return super().msg
            """,
        )
        with import_helper.ready_to_import(name="shadowed_super", source=source):
            import shadowed_super
        self.assertEqual(shadowed_super.C().method(), "truly super")
        import_helper.unload("shadowed_super")

    def test_shadowed_local(self):
        class super:
            msg = "quite super"

        class C:
            def method(self):
                return super().msg

        self.assertEqual(C().method(), "quite super")

    def test_shadowed_dynamic(self):
        class MySuper:
            msg = "super super"

        class C:
            def method(self):
                return super().msg

        with patch(f"{__name__}.super", MySuper) as m:
            self.assertEqual(C().method(), "super super")

    def test_shadowed_dynamic_two_arg(self):
        call_args = []
        class MySuper:
            def __init__(self, *args):
                call_args.append(args)
            msg = "super super"

        class C:
            def method(self):
                return super(1, 2).msg

        with patch(f"{__name__}.super", MySuper) as m:
            self.assertEqual(C().method(), "super super")
            self.assertEqual(call_args, [(1, 2)])

    def test_attribute_error(self):
        class C:
            def method(self):
                return super().msg

        with self.assertRaisesRegex(AttributeError, "'super' object has no attribute 'msg'"):
            C().method()

    def test_bad_first_arg(self):
        class C:
            def method(self):
                return super(1, self).method()

        with self.assertRaisesRegex(TypeError, "argument 1 must be a type"):
            C().method()

    def test_supercheck_fail(self):
        class C:
            def method(self, type_, obj):
                return super(type_, obj).method()

        c = C()
        err_msg = (
            r"super\(type, obj\): obj \({} {}\) is not "
            r"an instance or subtype of type \({}\)."
        )

        cases = (
            (int, c, int.__name__, C.__name__, "instance of"),
            # obj is instance of type
            (C, list(), C.__name__, list.__name__, "instance of"),
            # obj is type itself
            (C, list, C.__name__, list.__name__, "type"),
        )

        for case in cases:
            with self.subTest(case=case):
                type_, obj, type_str, obj_str, instance_or_type = case
                regex = err_msg.format(instance_or_type, obj_str, type_str)

                with self.assertRaisesRegex(TypeError, regex):
                    c.method(type_, obj)

    def test_super___class__(self):
        class C:
            def method(self):
                return super().__class__

        self.assertEqual(C().method(), super)

    def test_super_subclass___class__(self):
        class mysuper(super):
            pass

        class C:
            def method(self):
                return mysuper(C, self).__class__

        self.assertEqual(C().method(), mysuper)

    def test_unusual_getattro(self):
        class MyType(type):
            pass

        def test(name):
            mytype = MyType(name, (MyType,), {})
            super(MyType, type(mytype)).__setattr__(mytype, "bar", 1)
            self.assertEqual(mytype.bar, 1)

        for _ in range(ADAPTIVE_WARMUP_DELAY):
            test("foo1")

    def test_reassigned_new(self):
        class A:
            def __new__(cls):
                pass

            def __init_subclass__(cls):
                if "__new__" not in cls.__dict__:
                    cls.__new__ = cls.__new__

        class B(A):
            pass

        class C(B):
            def __new__(cls):
                return super().__new__(cls)

        for _ in range(ADAPTIVE_WARMUP_DELAY):
            C()

    def test_mixed_staticmethod_hierarchy(self):
        # This test is just a desugared version of `test_reassigned_new`
        class A:
            @staticmethod
            def some(cls, *args, **kwargs):
                self.assertFalse(args)
                self.assertFalse(kwargs)

        class B(A):
            def some(cls, *args, **kwargs):
                return super().some(cls, *args, **kwargs)

        class C(B):
            @staticmethod
            def some(cls):
                return super().some(cls)

        for _ in range(ADAPTIVE_WARMUP_DELAY):
            C.some(C)


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