#  fixtures: Fixtures with cleanups for testing and convenience.
#
# Copyright (c) 2010, Robert Collins <robertc@robertcollins.net>
#
# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause
# license at the users choice. A copy of both licenses are available in the
# project source as Apache-2.0 and BSD. You may not use this file except in
# compliance with one of these two licences.
#
# Unless required by applicable law or agreed to in writing, software
# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
# license you chose for the specific language governing permissions and
# limitations under that license.

import functools
import sys

import testtools
from testtools.matchers import Is

from fixtures import MonkeyPatch, TestWithFixtures

reference = 23

NEW_PY39_CLASSMETHOD = sys.version_info[:2] in (
    (3, 9),
    (3, 10),
) and not hasattr(sys, "pypy_version_info")


class C(object):
    def foo(self, arg):
        return arg

    @staticmethod
    def foo_static():
        pass

    @classmethod
    def foo_cls(cls):
        pass


class D(object):
    def bar(self):
        pass

    def bar_two_args(self, arg=None):
        return (self, arg)

    @classmethod
    def bar_cls(cls):
        return cls

    @classmethod
    def bar_cls_args(cls, *args):
        return (cls,) + args

    @staticmethod
    def bar_static():
        pass

    @staticmethod
    def bar_static_args(*args):
        return args

    def bar_self_referential(self, *args, **kwargs):
        self.bar()


INST_C = C()


def fake(*args):
    return args


def fake2(arg):
    pass


def fake_no_args():
    pass


def fake_no_args2():
    pass


@staticmethod
def fake_static():
    pass


class TestMonkeyPatch(testtools.TestCase, TestWithFixtures):
    def test_patch_and_restore(self):
        fixture = MonkeyPatch(
            'fixtures.tests._fixtures.test_monkeypatch.reference', 45
        )
        self.assertEqual(23, reference)
        fixture.setUp()
        try:
            self.assertEqual(45, reference)
        finally:
            fixture.cleanUp()
            self.assertEqual(23, reference)

    def test_patch_missing_attribute(self):
        fixture = MonkeyPatch(
            'fixtures.tests._fixtures.test_monkeypatch.new_attr', True
        )
        self.assertFalse('new_attr' in globals())
        fixture.setUp()
        try:
            self.assertEqual(True, new_attr)  # noqa: F821
        finally:
            fixture.cleanUp()
            self.assertFalse('new_attr' in globals())

    def test_delete_existing_attribute(self):
        fixture = MonkeyPatch(
            'fixtures.tests._fixtures.test_monkeypatch.reference',
            MonkeyPatch.delete,
        )
        self.assertEqual(23, reference)
        fixture.setUp()
        try:
            self.assertFalse('reference' in globals())
        finally:
            fixture.cleanUp()
            self.assertEqual(23, reference)

    def test_delete_missing_attribute(self):
        fixture = MonkeyPatch(
            'fixtures.tests._fixtures.test_monkeypatch.new_attr',
            MonkeyPatch.delete,
        )
        self.assertFalse('new_attr' in globals())
        fixture.setUp()
        try:
            self.assertFalse('new_attr' in globals())
        finally:
            fixture.cleanUp()
            self.assertFalse('new_attr' in globals())

    def _check_restored_static_or_class_method(
        self, oldmethod, oldmethod_inst, klass, methodname
    ):
        restored_method = getattr(klass, methodname)
        restored_method_inst = getattr(klass(), methodname)
        self.assertEqual(oldmethod, restored_method)
        self.assertEqual(oldmethod, restored_method_inst)
        self.assertEqual(oldmethod_inst, restored_method)
        self.assertEqual(oldmethod_inst, restored_method_inst)
        restored_method()
        restored_method_inst()

    def test_patch_staticmethod_with_staticmethod(self):
        oldmethod = C.foo_static
        oldmethod_inst = C().foo_static
        fixture = MonkeyPatch(
            'fixtures.tests._fixtures.test_monkeypatch.C.foo_static',
            D.bar_static,
        )
        with fixture:
            C.foo_static()
            C().foo_static()
        self._check_restored_static_or_class_method(
            oldmethod, oldmethod_inst, C, 'foo_static'
        )

    def test_patch_staticmethod_with_classmethod(self):
        oldmethod = C.foo_static
        oldmethod_inst = C().foo_static
        fixture = MonkeyPatch(
            'fixtures.tests._fixtures.test_monkeypatch.C.foo_static', D.bar_cls
        )
        with fixture:
            C.foo_static()
            C().foo_static()
        self._check_restored_static_or_class_method(
            oldmethod, oldmethod_inst, C, 'foo_static'
        )

    def test_patch_staticmethod_with_function(self):
        oldmethod = C.foo_static
        oldmethod_inst = C().foo_static
        fixture = MonkeyPatch(
            'fixtures.tests._fixtures.test_monkeypatch.C.foo_static',
            fake_no_args,
        )
        with fixture:
            C.foo_static()
            C().foo_static()
        self._check_restored_static_or_class_method(
            oldmethod, oldmethod_inst, C, 'foo_static'
        )

    def test_patch_staticmethod_with_boundmethod(self):
        oldmethod = C.foo_static
        oldmethod_inst = C().foo_static
        fixture = MonkeyPatch(
            'fixtures.tests._fixtures.test_monkeypatch.C.foo_static', D().bar
        )
        with fixture:
            C.foo_static()
            C().foo_static()
        self._check_restored_static_or_class_method(
            oldmethod, oldmethod_inst, C, 'foo_static'
        )

    def test_patch_classmethod_with_staticmethod(self):
        oldmethod = C.foo_cls
        oldmethod_inst = C().foo_cls
        fixture = MonkeyPatch(
            'fixtures.tests._fixtures.test_monkeypatch.C.foo_cls',
            D.bar_static_args,
        )
        with fixture:
            (cls,) = C.foo_cls()
            self.expectThat(cls, Is(C))
            (cls,) = C().foo_cls()
            self.expectThat(cls, Is(C))
        self._check_restored_static_or_class_method(
            oldmethod, oldmethod_inst, C, 'foo_cls'
        )

    def test_patch_classmethod_with_classmethod(self):
        oldmethod = C.foo_cls
        oldmethod_inst = C().foo_cls
        fixture = MonkeyPatch(
            'fixtures.tests._fixtures.test_monkeypatch.C.foo_cls',
            D.bar_cls_args,
        )
        with fixture:
            # Python 3.9 changes the behavior of the classmethod decorator so
            # that it now honours the descriptor binding protocol [1].
            # This means we're now going to call the monkeypatched classmethod
            # the way we would have if it hadn't been monkeypatched: simply
            # with the class
            #
            # https://bugs.python.org/issue19072
            if NEW_PY39_CLASSMETHOD:
                (cls,) = C.foo_cls()
                self.expectThat(cls, Is(D))
                (cls,) = C().foo_cls()
                self.expectThat(cls, Is(D))
            else:
                cls, target_class = C.foo_cls()
                self.expectThat(cls, Is(D))
                self.expectThat(target_class, Is(C))
                cls, target_class = C().foo_cls()
                self.expectThat(cls, Is(D))
                self.expectThat(target_class, Is(C))
        self._check_restored_static_or_class_method(
            oldmethod, oldmethod_inst, C, 'foo_cls'
        )

    def test_patch_classmethod_with_function(self):
        oldmethod = C.foo_cls
        oldmethod_inst = C().foo_cls
        fixture = MonkeyPatch(
            'fixtures.tests._fixtures.test_monkeypatch.C.foo_cls', fake
        )
        with fixture:
            (cls,) = C.foo_cls()
            self.expectThat(cls, Is(C))
            (cls, arg) = C().foo_cls(1)
            self.expectThat(cls, Is(C))
            self.assertEqual(1, arg)
        self._check_restored_static_or_class_method(
            oldmethod, oldmethod_inst, C, 'foo_cls'
        )

    def test_patch_classmethod_with_boundmethod(self):
        oldmethod = C.foo_cls
        oldmethod_inst = C().foo_cls
        d = D()
        fixture = MonkeyPatch(
            'fixtures.tests._fixtures.test_monkeypatch.C.foo_cls',
            d.bar_two_args,
        )
        with fixture:
            slf, cls = C.foo_cls()
            self.expectThat(slf, Is(d))
            # See note in test_patch_classmethod_with_classmethod on changes in
            # Python 3.9
            if NEW_PY39_CLASSMETHOD:
                self.expectThat(cls, Is(None))
            else:
                self.expectThat(cls, Is(C))
            slf, cls = C().foo_cls()
            self.expectThat(slf, Is(d))
            if NEW_PY39_CLASSMETHOD:
                self.expectThat(cls, Is(None))
            else:
                self.expectThat(cls, Is(C))
        self._check_restored_static_or_class_method(
            oldmethod, oldmethod_inst, C, 'foo_cls'
        )

    def test_patch_function_with_staticmethod(self):
        oldmethod = fake_no_args
        fixture = MonkeyPatch(
            'fixtures.tests._fixtures.test_monkeypatch.fake_no_args',
            D.bar_static,
        )
        with fixture:
            fake_no_args()
        self.assertEqual(oldmethod, fake_no_args)

    def test_patch_function_with_classmethod(self):
        oldmethod = fake_no_args
        fixture = MonkeyPatch(
            'fixtures.tests._fixtures.test_monkeypatch.fake_no_args', D.bar_cls
        )
        with fixture:
            fake_no_args()
        self.assertEqual(oldmethod, fake_no_args)

    def test_patch_function_with_function(self):
        oldmethod = fake_no_args
        fixture = MonkeyPatch(
            'fixtures.tests._fixtures.test_monkeypatch.fake_no_args',
            fake_no_args2,
        )
        with fixture:
            fake_no_args()
        self.assertEqual(oldmethod, fake_no_args)

    def test_patch_function_with_partial(self):
        oldmethod = fake_no_args
        fixture = MonkeyPatch(
            'fixtures.tests._fixtures.test_monkeypatch.fake_no_args',
            functools.partial(fake, 1),
        )
        with fixture:
            (ret,) = fake_no_args()
            self.assertEqual(1, ret)
        self.assertEqual(oldmethod, fake_no_args)

    def test_patch_function_with_boundmethod(self):
        oldmethod = fake_no_args
        fixture = MonkeyPatch(
            'fixtures.tests._fixtures.test_monkeypatch.fake_no_args', D().bar
        )
        with fixture:
            fake_no_args()
        self.assertEqual(oldmethod, fake_no_args)

    def test_patch_boundmethod_with_staticmethod(self):
        oldmethod = INST_C.foo
        fixture = MonkeyPatch(
            'fixtures.tests._fixtures.test_monkeypatch.INST_C.foo',
            D.bar_static,
        )
        with fixture:
            INST_C.foo()
        self.assertEqual(oldmethod, INST_C.foo)

    def test_patch_boundmethod_with_classmethod(self):
        oldmethod = INST_C.foo
        fixture = MonkeyPatch(
            'fixtures.tests._fixtures.test_monkeypatch.INST_C.foo', D.bar_cls
        )
        with fixture:
            INST_C.foo()
        self.assertEqual(oldmethod, INST_C.foo)

    def test_patch_boundmethod_with_function(self):
        oldmethod = INST_C.foo
        fixture = MonkeyPatch(
            'fixtures.tests._fixtures.test_monkeypatch.INST_C.foo',
            fake_no_args,
        )
        with fixture:
            INST_C.foo()
        self.assertEqual(oldmethod, INST_C.foo)

    def test_patch_boundmethod_with_boundmethod(self):
        oldmethod = INST_C.foo
        fixture = MonkeyPatch(
            'fixtures.tests._fixtures.test_monkeypatch.INST_C.foo', D().bar
        )
        with fixture:
            INST_C.foo()
        self.assertEqual(oldmethod, INST_C.foo)
        sentinel = object()
        self.assertEqual(sentinel, INST_C.__dict__.get('foo', sentinel))

    def test_patch_unboundmethod_with_staticmethod(self):
        oldmethod = C.foo
        fixture = MonkeyPatch(
            'fixtures.tests._fixtures.test_monkeypatch.C.foo',
            D.bar_static_args,
        )
        with fixture:
            target_self, arg = INST_C.foo(1)
            self.expectThat(target_self, Is(INST_C))
            self.assertEqual(1, arg)
        self.assertEqual(oldmethod, C.foo)

    def test_patch_unboundmethod_with_classmethod(self):
        oldmethod = C.foo
        fixture = MonkeyPatch(
            'fixtures.tests._fixtures.test_monkeypatch.C.foo', D.bar_cls_args
        )
        with fixture:
            c = C()
            cls, target_self, arg = c.foo(1)
            self.expectThat(cls, Is(D))
            self.expectThat(target_self, Is(c))
            self.assertEqual(1, arg)
        self.assertEqual(oldmethod, C.foo)

    def test_patch_unboundmethod_with_function(self):
        oldmethod = C.foo
        fixture = MonkeyPatch(
            'fixtures.tests._fixtures.test_monkeypatch.C.foo', fake
        )
        with fixture:
            c = C()
            target_self, arg = c.foo(1)
            self.expectThat(target_self, Is(c))
            self.assertTrue(1, arg)
        self.assertEqual(oldmethod, C.foo)

    def test_patch_unboundmethod_with_boundmethod(self):
        oldmethod = C.foo
        d = D()
        fixture = MonkeyPatch(
            'fixtures.tests._fixtures.test_monkeypatch.C.foo', d.bar_two_args
        )
        with fixture:
            c = C()
            slf, target_self = c.foo()
            self.expectThat(slf, Is(d))
            self.expectThat(target_self, Is(c))
        self.assertEqual(oldmethod, C.foo)

    def test_double_patch_instancemethod(self):
        oldmethod = C.foo
        oldmethod_inst = C().foo
        fixture1 = MonkeyPatch(
            'fixtures.tests._fixtures.test_monkeypatch.C.foo', fake
        )
        fixture2 = MonkeyPatch(
            'fixtures.tests._fixtures.test_monkeypatch.C.foo', fake2
        )
        with fixture1:
            with fixture2:
                C().foo()
        self.assertEqual(oldmethod, C.foo)
        # The method address changes with each instantiation of C, and method
        # equivalence just tests that. Compare the code objects instead.
        self.assertEqual(oldmethod_inst.__code__, C().foo.__code__)

    def test_double_patch_staticmethod(self):
        oldmethod = C.foo_static
        oldmethod_inst = C().foo_static
        fixture1 = MonkeyPatch(
            'fixtures.tests._fixtures.test_monkeypatch.C.foo_static',
            fake_no_args,
        )
        fixture2 = MonkeyPatch(
            'fixtures.tests._fixtures.test_monkeypatch.C.foo_static',
            fake_static,
        )
        with fixture1:
            with fixture2:
                C.foo_static()
                C().foo_static()
        self._check_restored_static_or_class_method(
            oldmethod, oldmethod_inst, C, 'foo_static'
        )

    def test_double_patch_classmethod(self):
        oldmethod = C.foo_cls
        oldmethod_inst = C().foo_cls
        fixture1 = MonkeyPatch(
            'fixtures.tests._fixtures.test_monkeypatch.C.foo_cls', fake
        )
        fixture2 = MonkeyPatch(
            'fixtures.tests._fixtures.test_monkeypatch.C.foo_cls', fake2
        )
        with fixture1:
            with fixture2:
                C.foo_cls()
                C().foo_cls()
        self._check_restored_static_or_class_method(
            oldmethod, oldmethod_inst, C, 'foo_cls'
        )

    def test_patch_c_foo_with_instance_d_bar_self_referential(self):
        oldmethod = C.foo
        oldmethod_inst = C().foo
        fixture = MonkeyPatch(
            'fixtures.tests._fixtures.test_monkeypatch.C.foo',
            D().bar_self_referential,
        )
        with fixture:
            C().foo()
        self.assertEqual(oldmethod, C.foo)
        # The method address changes with each instantiation of C, and method
        # equivalence just tests that. Compare the code objects instead.
        self.assertEqual(oldmethod_inst.__code__, C().foo.__code__)
