import os  # @NoMove
import sys  # @NoMove
import pytest

from _pydevd_bundle import pydevd_reload
import tempfile
import unittest


SAMPLE_CODE = """
class C:
    def foo(self):
        return 0

    @classmethod
    def bar(cls):
        return (0, 0)

    @staticmethod
    def stomp():
        return (0, 0, 0)

    def unchanged(self):
        return 'unchanged'
"""

from _pydevd_bundle.pydevd_constants import IS_JYTHON, IS_IRONPYTHON

@pytest.mark.skipif(IS_JYTHON or IS_IRONPYTHON, reason='CPython related test')
class Test(unittest.TestCase):


    def setUp(self):
        unittest.TestCase.setUp(self)
        self.tempdir = None
        self.save_path = None
        self.tempdir = tempfile.mkdtemp()
        self.save_path = list(sys.path)
        sys.path.append(self.tempdir)
        try:
            del sys.modules['x']
        except:
            pass


    def tearDown(self):
        unittest.TestCase.tearDown(self)
        sys.path = self.save_path
        try:
            del sys.modules['x']
        except:
            pass

    def make_mod(self, name="x", repl=None, subst=None, sample=SAMPLE_CODE):
        fn = os.path.join(self.tempdir, name + ".py")
        f = open(fn, "w")
        if repl is not None and subst is not None:
            sample = sample.replace(repl, subst)
        try:
            f.write(sample)
        finally:
            f.close()


    def test_pydevd_reload(self):

        self.make_mod()
        import x  # @UnresolvedImport

        C = x.C
        COut = C
        Cfoo = C.foo
        Cbar = C.bar
        Cstomp = C.stomp

        def check2(expected):
            C = x.C
            Cfoo = C.foo
            Cbar = C.bar
            Cstomp = C.stomp
            b = C()
            bfoo = b.foo
            self.assertEqual(expected, b.foo())
            self.assertEqual(expected, bfoo())
            self.assertEqual(expected, Cfoo(b))

        def check(expected):
            b = COut()
            bfoo = b.foo
            self.assertEqual(expected, b.foo())
            self.assertEqual(expected, bfoo())
            self.assertEqual(expected, Cfoo(b))
            self.assertEqual((expected, expected), Cbar())
            self.assertEqual((expected, expected, expected), Cstomp())
            check2(expected)

        check(0)

        # modify mod and reload
        count = 0
        while count < 1:
            count += 1
            self.make_mod(repl="0", subst=str(count))
            pydevd_reload.xreload(x)
            check(count)


    def test_pydevd_reload2(self):

        self.make_mod()
        import x  # @UnresolvedImport

        c = x.C()
        cfoo = c.foo
        self.assertEqual(0, c.foo())
        self.assertEqual(0, cfoo())

        self.make_mod(repl="0", subst='1')
        pydevd_reload.xreload(x)
        self.assertEqual(1, c.foo())
        self.assertEqual(1, cfoo())

    def test_pydevd_reload3(self):
        class F:
            def m1(self):
                return 1
        class G:
            def m1(self):
                return 2

        self.assertEqual(F().m1(), 1)
        pydevd_reload.Reload(None)._update(None, None, F, G)
        self.assertEqual(F().m1(), 2)


    def test_pydevd_reload4(self):
        class F:
            pass
        F.m1 = lambda a:None
        class G:
            pass
        G.m1 = lambda a:10

        self.assertEqual(F().m1(), None)
        pydevd_reload.Reload(None)._update(None, None, F, G)
        self.assertEqual(F().m1(), 10)



    def test_if_code_obj_equals(self):
        class F:
            def m1(self):
                return 1
        class G:
            def m1(self):
                return 1
        class H:
            def m1(self):
                return 2

        if hasattr(F.m1, 'func_code'):
            self.assertTrue(pydevd_reload.code_objects_equal(F.m1.func_code, G.m1.func_code))
            self.assertFalse(pydevd_reload.code_objects_equal(F.m1.func_code, H.m1.func_code))
        else:
            self.assertTrue(pydevd_reload.code_objects_equal(F.m1.__code__, G.m1.__code__))
            self.assertFalse(pydevd_reload.code_objects_equal(F.m1.__code__, H.m1.__code__))



    def test_metaclass(self):

        class Meta(type):
            def __init__(cls, name, bases, attrs):
                super(Meta, cls).__init__(name, bases, attrs)

        class F:
            __metaclass__ = Meta

            def m1(self):
                return 1


        class G:
            __metaclass__ = Meta

            def m1(self):
                return 2

        self.assertEqual(F().m1(), 1)
        pydevd_reload.Reload(None)._update(None, None, F, G)
        self.assertEqual(F().m1(), 2)



    def test_change_hierarchy(self):

        class F(object):

            def m1(self):
                return 1


        class B(object):
            def super_call(self):
                return 2

        class G(B):

            def m1(self):
                return self.super_call()

        self.assertEqual(F().m1(), 1)
        old = pydevd_reload.notify_error
        self._called = False
        def on_error(*args):
            self._called = True
        try:
            pydevd_reload.notify_error = on_error
            pydevd_reload.Reload(None)._update(None, None, F, G)
            self.assertTrue(self._called)
        finally:
            pydevd_reload.notify_error = old


    def test_change_hierarchy_old_style(self):

        class F:

            def m1(self):
                return 1


        class B:
            def super_call(self):
                return 2

        class G(B):

            def m1(self):
                return self.super_call()


        self.assertEqual(F().m1(), 1)
        old = pydevd_reload.notify_error
        self._called = False
        def on_error(*args):
            self._called = True
        try:
            pydevd_reload.notify_error = on_error
            pydevd_reload.Reload(None)._update(None, None, F, G)
            self.assertTrue(self._called)
        finally:
            pydevd_reload.notify_error = old


    def test_create_class(self):
        SAMPLE_CODE1 = """
class C:
    def foo(self):
        return 0
"""
        # Creating a new class and using it from old class
        SAMPLE_CODE2 = """
class B:
    pass

class C:
    def foo(self):
        return B
"""

        self.make_mod(sample=SAMPLE_CODE1)
        import x  # @UnresolvedImport
        foo = x.C().foo
        self.assertEqual(foo(), 0)
        self.make_mod(sample=SAMPLE_CODE2)
        pydevd_reload.xreload(x)
        self.assertEqual(foo().__name__, 'B')

    def test_create_class2(self):
        SAMPLE_CODE1 = """
class C(object):
    def foo(self):
        return 0
"""
        # Creating a new class and using it from old class
        SAMPLE_CODE2 = """
class B(object):
    pass

class C(object):
    def foo(self):
        return B
"""

        self.make_mod(sample=SAMPLE_CODE1)
        import x  # @UnresolvedImport
        foo = x.C().foo
        self.assertEqual(foo(), 0)
        self.make_mod(sample=SAMPLE_CODE2)
        pydevd_reload.xreload(x)
        self.assertEqual(foo().__name__, 'B')

    def test_parent_function(self):
        SAMPLE_CODE1 = """
class B(object):
    def foo(self):
        return 0

class C(B):
    def call(self):
        return self.foo()
"""
        # Creating a new class and using it from old class
        SAMPLE_CODE2 = """
class B(object):
    def foo(self):
        return 0
    def bar(self):
        return 'bar'

class C(B):
    def call(self):
        return self.bar()
"""

        self.make_mod(sample=SAMPLE_CODE1)
        import x  # @UnresolvedImport
        call = x.C().call
        self.assertEqual(call(), 0)
        self.make_mod(sample=SAMPLE_CODE2)
        pydevd_reload.xreload(x)
        self.assertEqual(call(), 'bar')


    def test_update_constant(self):
        SAMPLE_CODE1 = """
CONSTANT = 1

class B(object):
    def foo(self):
        return CONSTANT
"""
        SAMPLE_CODE2 = """
CONSTANT = 2

class B(object):
    def foo(self):
        return CONSTANT
"""

        self.make_mod(sample=SAMPLE_CODE1)
        import x  # @UnresolvedImport
        foo = x.B().foo
        self.assertEqual(foo(), 1)
        self.make_mod(sample=SAMPLE_CODE2)
        pydevd_reload.xreload(x)
        self.assertEqual(foo(), 1) #Just making it explicit we don't reload constants.


    def test_update_constant_with_custom_code(self):
        SAMPLE_CODE1 = """
CONSTANT = 1

class B(object):
    def foo(self):
        return CONSTANT
"""
        SAMPLE_CODE2 = """
CONSTANT = 2

def __xreload_old_new__(namespace, name, old, new):
    if name == 'CONSTANT':
        namespace[name] = new

class B(object):
    def foo(self):
        return CONSTANT
"""

        self.make_mod(sample=SAMPLE_CODE1)
        import x  # @UnresolvedImport
        foo = x.B().foo
        self.assertEqual(foo(), 1)
        self.make_mod(sample=SAMPLE_CODE2)
        pydevd_reload.xreload(x)
        self.assertEqual(foo(), 2) #Actually updated it now!


    def test_reload_custom_code_after_changes(self):
        SAMPLE_CODE1 = """
CONSTANT = 1

class B(object):
    def foo(self):
        return CONSTANT
"""
        SAMPLE_CODE2 = """
CONSTANT = 1

def __xreload_after_reload_update__(namespace):
    namespace['CONSTANT'] = 2

class B(object):
    def foo(self):
        return CONSTANT
"""

        self.make_mod(sample=SAMPLE_CODE1)
        import x  # @UnresolvedImport
        foo = x.B().foo
        self.assertEqual(foo(), 1)
        self.make_mod(sample=SAMPLE_CODE2)
        pydevd_reload.xreload(x)
        self.assertEqual(foo(), 2) #Actually updated it now!


    def test_reload_custom_code_after_changes_in_class(self):
        SAMPLE_CODE1 = """

class B(object):
    CONSTANT = 1

    def foo(self):
        return self.CONSTANT
"""
        SAMPLE_CODE2 = """


class B(object):
    CONSTANT = 1

    @classmethod
    def __xreload_after_reload_update__(cls):
        cls.CONSTANT = 2

    def foo(self):
        return self.CONSTANT
"""

        self.make_mod(sample=SAMPLE_CODE1)
        import x  # @UnresolvedImport
        foo = x.B().foo
        self.assertEqual(foo(), 1)
        self.make_mod(sample=SAMPLE_CODE2)
        pydevd_reload.xreload(x)
        self.assertEqual(foo(), 2) #Actually updated it now!


    def test_update_constant_with_custom_code2(self):
        SAMPLE_CODE1 = """

class B(object):
    CONSTANT = 1

    def foo(self):
        return self.CONSTANT
"""
        SAMPLE_CODE2 = """


class B(object):

    CONSTANT = 2

    def __xreload_old_new__(cls, name, old, new):
        if name == 'CONSTANT':
            cls.CONSTANT = new
    __xreload_old_new__ = classmethod(__xreload_old_new__)

    def foo(self):
        return self.CONSTANT
"""

        self.make_mod(sample=SAMPLE_CODE1)
        import x  # @UnresolvedImport
        foo = x.B().foo
        self.assertEqual(foo(), 1)
        self.make_mod(sample=SAMPLE_CODE2)
        pydevd_reload.xreload(x)
        self.assertEqual(foo(), 2) #Actually updated it now!


    def test_update_with_slots(self):
        SAMPLE_CODE1 = """
class B(object):

    __slots__ = ['bar']

"""
        SAMPLE_CODE2 = """
class B(object):

    __slots__ = ['bar', 'foo']

    def m1(self):
        self.bar = 10
        return 1

"""

        self.make_mod(sample=SAMPLE_CODE1)
        import x  # @UnresolvedImport
        B = x.B
        self.make_mod(sample=SAMPLE_CODE2)
        pydevd_reload.xreload(x)
        b = B()
        self.assertEqual(1, b.m1())
        self.assertEqual(10, b.bar)
        self.assertRaises(Exception, setattr, b, 'foo', 20) #__slots__ can't be updated


