#!/usr/bin/env python3
# Copyright (c) Facebook, Inc. and its affiliates. (http://www.facebook.com)
import unittest
import weakref
from unittest.mock import MagicMock, Mock


class WeakRefTests(unittest.TestCase):
    def test_ref_dunder_callback_readonly(self):
        class C:
            pass

        def callback(*args):
            pass

        obj = C()
        ref = weakref.ref(obj)
        with self.assertRaises(AttributeError):
            ref.__callback__ = callback

    def test_ref_dunder_callback_with_callback_returns_callback(self):
        class C:
            pass

        def callback(*args):
            pass

        obj = C()
        ref = weakref.ref(obj, callback)
        self.assertIs(ref.__callback__, callback)

    def test_ref_dunder_callback_without_callback_returns_none(self):
        class C:
            pass

        obj = C()
        ref = weakref.ref(obj)
        self.assertIsNone(ref.__callback__)

    def test_dunder_callback_with_subtype_returns_callback(self):
        class SubRef(weakref.ref):
            pass

        class C:
            pass

        def callback(wr):
            pass

        obj = C()
        ref = SubRef(obj, callback)
        self.assertIs(ref.__callback__, callback)

    def test_dunder_callback_with_subtype_passes_subtype(self):
        class SubRef(weakref.ref):
            pass

        class C:
            pass

        def callback(wr):
            wr.callback_arg = wr

        ref = SubRef(C(), callback)
        try:
            from _builtins import _gc

            _gc()
        except ImportError:
            pass

        self.assertIs(ref.callback_arg, ref)

    def test_ref_dunder_call_with_non_ref_raises_type_error(self):
        self.assertRaisesRegex(
            TypeError,
            "'__call__' .* 'weakref' object.* a 'str'",
            weakref.ref.__call__,
            "not a weakref",
        )

    def test_dunder_eq_proxies_dunder_eq(self):
        class C:
            def __eq__(self, other):
                return self is other

        obj1 = C()
        obj2 = C()
        ref1 = weakref.ref(obj1)
        ref2 = weakref.ref(obj2)
        obj1.__eq__ = MagicMock()
        self.assertIs(ref1.__eq__(ref2), False)
        obj1.__eq__.called_once_with(obj1, obj2)
        self.assertIs(ref1.__eq__(ref1), True)
        obj1.__eq__.called_once_with(obj1, obj1)

    def test_dunder_eq_with_non_ref_returns_not_implemented(self):
        class C:
            pass

        obj = C()
        ref = weakref.ref(obj)
        not_a_ref = object()
        self.assertIs(ref.__eq__(not_a_ref), NotImplemented)

    def test_dunder_ge_always_returns_not_implemented(self):
        class C:
            pass

        obj = C()
        ref = weakref.ref(obj)
        not_a_ref = object()
        self.assertIs(ref.__ge__(ref), NotImplemented)
        self.assertIs(ref.__ge__(not_a_ref), NotImplemented)

    def test_dunder_gt_always_returns_not_implemented(self):
        class C:
            pass

        obj = C()
        ref = weakref.ref(obj)
        not_a_ref = object()
        self.assertIs(ref.__gt__(ref), NotImplemented)
        self.assertIs(ref.__gt__(not_a_ref), NotImplemented)

    def test_dunder_le_always_returns_not_implemented(self):
        class C:
            pass

        obj = C()
        ref = weakref.ref(obj)
        not_a_ref = object()
        self.assertIs(ref.__le__(ref), NotImplemented)
        self.assertIs(ref.__le__(not_a_ref), NotImplemented)

    def test_dunder_lt_always_returns_not_implemented(self):
        class C:
            pass

        obj = C()
        ref = weakref.ref(obj)
        not_a_ref = object()
        self.assertIs(ref.__lt__(ref), NotImplemented)
        self.assertIs(ref.__lt__(not_a_ref), NotImplemented)

    def test_dunder_ne_proxies_dunder_ne(self):
        class C:
            def __ne__(self, other):
                return self is other

        obj1 = C()
        obj2 = C()
        ref1 = weakref.ref(obj1)
        ref2 = weakref.ref(obj2)
        obj1.__ne__ = MagicMock()
        self.assertIs(ref1.__ne__(ref2), False)
        obj1.__ne__.called_once_with(obj1, obj2)
        self.assertIs(ref1.__ne__(ref1), True)
        obj1.__ne__.called_once_with(obj1, obj1)

    def test_dunder_ne_with_non_ref_returns_not_implemented(self):
        class C:
            pass

        obj = C()
        ref = weakref.ref(obj)
        not_a_ref = object()
        self.assertIs(ref.__ne__(not_a_ref), NotImplemented)

    def test_dunder_new_with_subtype_return_subtype_instance(self):
        class SubRef(weakref.ref):
            pass

        class C:
            def __eq__(self, other):
                return "C.__eq__"

        c = C()
        sub_ref = SubRef(c)
        self.assertIsInstance(sub_ref, SubRef)
        self.assertIsInstance(sub_ref, weakref.ref)

        ref = weakref.ref(c)
        self.assertEqual(sub_ref.__eq__(ref), "C.__eq__")

        sub_ref.new_attribute = 50
        self.assertEqual(sub_ref.new_attribute, 50)

    def test_dunder_new_with_non_type_raises_type_error(self):
        with self.assertRaises(TypeError):
            weakref.ref.__new__("not a type object")

    def test_dunder_new_with_non_ref_subtype_raises_type_error(self):
        with self.assertRaises(TypeError):
            weakref.ref.__new__(list)

    def test_dunder_new_with_int_raises_type_error(self):
        with self.assertRaisesRegex(
            TypeError, "cannot create weak reference to 'int' object"
        ):
            weakref.ref.__new__(weakref.ref, 42)

    def test_hash_on_proxy_not_callable_object_raises_type_error(self):
        with self.assertRaises(TypeError) as context:

            class NotCallable:
                def get_name(self):
                    return "NotCallableObject"

            not_callable = NotCallable()
            proxy = weakref.proxy(not_callable)
            hash(proxy)
        self.assertEqual(str(context.exception), "unhashable type: 'weakproxy'")

    def test_proxy_not_callable_object_returns_proxy_type(self):
        class NotCallable:
            def get_name(self):
                return "NotCallableObject"

        not_callable = NotCallable()
        proxy = weakref.proxy(not_callable)
        self.assertEqual(type(proxy), weakref.ProxyType)

    def test_proxy_calls_to_dunder_functions(self):
        class C:
            def __add__(self, another):
                return 50 + another

        c = C()
        proxy = weakref.proxy(c)
        self.assertEqual(proxy + 5, 55)

    def test_proxy_with_hash_raises_type_error(self):
        with self.assertRaises(TypeError) as context:

            class C:
                pass

            c = C()
            hash(weakref.proxy(c))
        self.assertEqual(str(context.exception), "unhashable type: 'weakproxy'")

    def test_proxy_dunder_hash_function_access_suceeds(self):
        class C:
            pass

        c = C()
        m = c.__hash__()
        self.assertNotEqual(m, 0)

    def test_proxy_field_access(self):
        class C:
            def __init__(self):
                self.field = "field_value"

        c = C()
        proxy = weakref.proxy(c)
        self.assertEqual(proxy.field, "field_value")

    def test_proxy_instance_method_call(self):
        class C:
            def method(self):
                return "method_return"

        c = C()
        proxy = weakref.proxy(c)
        self.assertEqual(proxy.method(), "method_return")

    def test_hash_on_proxy_callable_object_raises_type_error(self):
        with self.assertRaises(TypeError) as context:

            class Callable:
                def __call__(self):
                    return "CallableObject"

            callable = Callable()
            proxy = weakref.proxy(callable)
            hash(proxy)
        self.assertEqual(str(context.exception), "unhashable type: 'weakcallableproxy'")

    def test_proxy_callable_object_returns_callable_proxy_type(self):
        class Callable:
            def __call__(self):
                return "CallableObject"

        callable = Callable()
        proxy = weakref.proxy(callable)
        self.assertTrue(isinstance(proxy, weakref.CallableProxyType))

    def test_proxy_callable_object_returns_callable_object(self):
        class Callable:
            def __call__(self):
                return "CallableObject"

        callable_obj = Callable()
        proxy = weakref.proxy(callable_obj)
        self.assertEqual(proxy(), "CallableObject")

    def test_hash_returns_referent_hash(self):
        class C:
            def __hash__(self):
                return 123456

        i = C()
        r = weakref.ref(i)
        self.assertEqual(hash(r), hash(i))

    def test_hash_picks_correct_dunder_hash(self):
        class C:
            def __hash__(self):
                raise Exception("Should not pick this")

        r = weakref.ref(C)
        self.assertEqual(hash(r), hash(C))

    def test_hash_caches_referent_hash(self):
        m = Mock()
        m.__hash__ = MagicMock(return_value=99)
        r = weakref.ref(m)
        self.assertEqual(hash(r), 99)
        self.assertEqual(hash(r), 99)
        m.__hash__.assert_called_once()

    # TODO(T43270097): Add a test for callback.


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