#  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)
        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__)
