import inspect
import json
import os
import sys
import unittest

import mitogen.core
import mitogen.master

import testlib


class ConstructorTest(testlib.TestCase):
    klass = mitogen.master.ModuleFinder

    def test_simple(self):
        self.klass()


class ReprTest(testlib.TestCase):
    klass = mitogen.master.ModuleFinder

    def test_simple(self):
        self.assertEqual('ModuleFinder()', repr(self.klass()))


class IsStdlibNameTest(testlib.TestCase):
    func = staticmethod(mitogen.master.is_stdlib_name)

    def test_builtin(self):
        import sys
        self.assertTrue(self.func('sys'))

    def test_stdlib_1(self):
        import logging
        self.assertTrue(self.func('logging'))

    def test_stdlib_2(self):
        # virtualenv only symlinks some paths to its local site-packages
        # directory. Ensure both halves of the search path return the correct
        # result.
        import email
        self.assertTrue(self.func('email'))

    def test_mitogen_core(self):
        import mitogen.core
        self.assertFalse(self.func('mitogen.core'))

    def test_mitogen_fakessh(self):
        import mitogen.fakessh
        self.assertFalse(self.func('mitogen.fakessh'))

    @unittest.skipIf(sys.version_info < (3, 3), 'Requires PEP 420 support')
    def test_implicit_namespace_pkg(self):
        import testmods.implicit_namespace_pkg.sub_pkg1
        self.assertFalse(self.func('testmods.implicit_namespace_pkg'))


class GetMainModuleDefectivePython3x(testlib.TestCase):
    klass = mitogen.master.DefectivePython3xMainMethod

    def call(self, fullname):
        return self.klass().find(fullname)

    def test_builtin(self):
        self.assertEqual(None, self.call('sys'))

    def test_not_main(self):
        self.assertEqual(None, self.call('mitogen'))

    def test_main(self):
        import __main__

        path, source, is_pkg = self.call('__main__')
        self.assertIsNotNone(path)
        self.assertTrue(os.path.exists(path))
        self.assertEqual(path, __main__.__file__)
        fp = open(path, 'rb')
        try:
            self.assertEqual(source, fp.read())
        finally:
            fp.close()
        self.assertFalse(is_pkg)


class PkgutilMethodTest(testlib.TestCase):
    klass = mitogen.master.PkgutilMethod

    def call(self, fullname):
        return self.klass().find(fullname)

    def test_empty_source_pkg(self):
        path, src, is_pkg = self.call('testmods.module_finder_testmod')
        self.assertEqual(path,
            os.path.join(testlib.TESTMODS_DIR, 'module_finder_testmod/__init__.py'))
        self.assertEqual(mitogen.core.b(''), src)
        self.assertTrue(is_pkg)

    def test_empty_source_module(self):
        path, src, is_pkg = self.call('testmods.module_finder_testmod.empty_mod')
        self.assertEqual(path,
            os.path.join(testlib.TESTMODS_DIR, 'module_finder_testmod/empty_mod.py'))
        self.assertEqual(mitogen.core.b(''), src)
        self.assertFalse(is_pkg)

    def test_regular_mod(self):
        from testmods.module_finder_testmod import regular_mod
        path, src, is_pkg = self.call('testmods.module_finder_testmod.regular_mod')
        self.assertEqual(path,
            os.path.join(testlib.TESTMODS_DIR, 'module_finder_testmod/regular_mod.py'))
        self.assertEqual(mitogen.core.to_text(src),
                          inspect.getsource(regular_mod))
        self.assertFalse(is_pkg)


class SysModulesMethodTest(testlib.TestCase):
    klass = mitogen.master.SysModulesMethod

    def call(self, fullname):
        return self.klass().find(fullname)

    def test_main(self):
        import __main__
        path, src, is_pkg = self.call('__main__')
        self.assertEqual(path, __main__.__file__)

        # linecache adds a line ending to the final line if one is missing.
        with open(path, 'rb') as f:
            actual_src = f.read()
        if actual_src[-1:] != mitogen.core.b('\n'):
            actual_src += mitogen.core.b('\n')

        self.assertEqual(src, actual_src)
        self.assertFalse(is_pkg)

    def test_dylib_fails(self):
        # _socket comes from a .so
        import _socket
        tup = self.call('_socket')
        self.assertIsNone(tup)

    def test_builtin_fails(self):
        # sys is built-in
        tup = self.call('sys')
        self.assertIsNone(tup)


class ParentEnumerationMixin(object):
    def call(self, fullname):
        return self.klass().find(fullname)

    def test_main_fails(self):
        import __main__
        self.assertIsNone(self.call('__main__'))

    def test_dylib_fails(self):
        # _socket comes from a .so
        import _socket
        tup = self.call('_socket')
        self.assertIsNone(tup)

    def test_builtin_fails(self):
        # sys is built-in
        tup = self.call('sys')
        self.assertIsNone(tup)

    def test_plumbum_colors_like_pkg_succeeds(self):
        # plumbum has been eating too many rainbow-colored pills
        import testmods.pkg_like_plumbum.colors
        path, src, is_pkg = self.call('testmods.pkg_like_plumbum.colors')
        modpath = os.path.join(testlib.TESTMODS_DIR, 'pkg_like_plumbum/colors.py')
        self.assertEqual(path, modpath)

        with open(modpath, 'rb') as f:
            self.assertEqual(src, f.read())
        self.assertFalse(is_pkg)

    def test_ansible_module_utils_distro_succeeds(self):
        # #590: a package that turns itself into a module.
        import testmods.pkg_like_ansible.module_utils.distro as d
        self.assertEqual(d.I_AM, "the module that replaced the package")
        self.assertEqual(
            sys.modules['testmods.pkg_like_ansible.module_utils.distro'].__name__,
            'testmods.pkg_like_ansible.module_utils.distro._distro'
        )

        # ensure we can resolve the subpackage.
        path, src, is_pkg = self.call('testmods.pkg_like_ansible.module_utils.distro')
        modpath = os.path.join(testlib.TESTMODS_DIR,
            'pkg_like_ansible/module_utils/distro/__init__.py')
        self.assertEqual(path, modpath)
        with open(modpath, 'rb') as f:
            self.assertEqual(src, f.read())
        self.assertEqual(is_pkg, True)

        # ensure we can resolve a child of the subpackage.
        path, src, is_pkg = self.call(
            'testmods.pkg_like_ansible.module_utils.distro._distro'
        )
        modpath = os.path.join(testlib.TESTMODS_DIR,
            'pkg_like_ansible/module_utils/distro/_distro.py')
        self.assertEqual(path, modpath)
        with open(modpath, 'rb') as f:
            self.assertEqual(src, f.read())
        self.assertEqual(is_pkg, False)

    def test_ansible_module_utils_system_distro_succeeds(self):
        # #590: a package that turns itself into a module.
        # #590: a package that turns itself into a module.
        import testmods.pkg_like_ansible.module_utils.sys_distro as d
        self.assertEqual(d.I_AM, "the system module that replaced the subpackage")
        self.assertEqual(
            sys.modules['testmods.pkg_like_ansible.module_utils.sys_distro'].__name__,
            'testmod_system_distro',
        )

        # ensure we can resolve the subpackage.
        path, src, is_pkg = self.call('testmods.pkg_like_ansible.module_utils.sys_distro')
        modpath = os.path.join(testlib.TESTMODS_DIR,
            'pkg_like_ansible/module_utils/sys_distro/__init__.py')
        self.assertEqual(path, modpath)
        with open(modpath, 'rb') as f:
            self.assertEqual(src, f.read())
        self.assertEqual(is_pkg, True)

        # ensure we can resolve a child of the subpackage.
        path, src, is_pkg = self.call(
            'testmods.pkg_like_ansible.module_utils.sys_distro._distro'
        )
        modpath = os.path.join(testlib.TESTMODS_DIR,
            'pkg_like_ansible/module_utils/sys_distro/_distro.py')
        self.assertEqual(path, modpath)
        with open(modpath, 'rb') as f:
            self.assertEqual(src, f.read())
        self.assertEqual(is_pkg, False)


@unittest.skipIf(sys.version_info >= (3, 4), 'Superceded in Python >= 3.4')
class ParentImpEnumerationMethodTest(ParentEnumerationMixin, testlib.TestCase):
    klass = mitogen.master.ParentImpEnumerationMethod


@unittest.skipIf(sys.version_info < (3, 4), 'Requires ModuleSpec, Python 3.4+')
class ParentSpecEnumerationMethodTest(ParentEnumerationMixin, testlib.TestCase):
    klass = mitogen.master.ParentSpecEnumerationMethod


class ResolveRelPathTest(testlib.TestCase):
    klass = mitogen.master.ModuleFinder

    def call(self, fullname, level):
        return self.klass().resolve_relpath(fullname, level)

    def test_empty(self):
        self.assertEqual('', self.call('', 0))
        self.assertEqual('', self.call('', 1))
        self.assertEqual('', self.call('', 2))

    def test_absolute(self):
        self.assertEqual('', self.call('email.utils', 0))

    def test_rel1(self):
        self.assertEqual('email.', self.call('email.utils', 1))

    def test_rel2(self):
        self.assertEqual('', self.call('email.utils', 2))

    def test_rel_overflow(self):
        self.assertEqual('', self.call('email.utils', 3))


class FakeSshTest(testlib.TestCase):
    klass = mitogen.master.ModuleFinder

    def call(self, fullname):
        return self.klass().find_related_imports(fullname)

    def test_simple(self):
        import mitogen.fakessh
        related = self.call('mitogen.fakessh')
        self.assertEqual(related, [
            'mitogen',
            'mitogen.core',
            'mitogen.parent',
        ])


class FindRelatedTest(testlib.TestCase):
    klass = mitogen.master.ModuleFinder

    def call(self, fullname):
        return self.klass().find_related(fullname)

    SIMPLE_EXPECT = set([
        u'mitogen',
        u'mitogen.core',
        u'mitogen.parent',
    ])

    if sys.version_info < (2, 7):
        SIMPLE_EXPECT.add('mitogen.compat')
        SIMPLE_EXPECT.add('mitogen.compat.tokenize')
    if sys.version_info < (2, 6):
        SIMPLE_EXPECT.add('mitogen.compat')
        SIMPLE_EXPECT.add('mitogen.compat.pkgutil')

    def test_simple(self):
        import mitogen.fakessh
        related = self.call('mitogen.fakessh')
        self.assertEqual(set(related), self.SIMPLE_EXPECT)


class DjangoMixin(object):
    WEBPROJECT_PATH = os.path.join(testlib.TESTMODS_DIR, 'webproject')

    @classmethod
    def modules_expected_path(cls):
        if sys.version_info[0:2] < (3, 0):
            modules_expected_filename = 'modules_expected_py2x.json'
        elif sys.version_info[0:2] <= (3, 6):
            modules_expected_filename = 'modules_expected_py3x-legacy.json'
        elif sys.version_info[0:2] >= (3, 10):
            modules_expected_filename = 'modules_expected_py3x-new.json'
        return os.path.join(cls.WEBPROJECT_PATH, modules_expected_filename)

    @classmethod
    def setUpClass(cls):
        super(DjangoMixin, cls).setUpClass()
        sys.path.append(cls.WEBPROJECT_PATH)
        os.environ['DJANGO_SETTINGS_MODULE'] = 'webproject.settings'
        with open(cls.modules_expected_path(), 'rb') as f:
            cls.MODULES_EXPECTED = json.load(f)

    @classmethod
    def tearDownClass(cls):
        sys.path.remove(cls.WEBPROJECT_PATH)
        del os.environ['DJANGO_SETTINGS_MODULE']
        super(DjangoMixin, cls).tearDownClass()


class DjangoFindRelatedTest(DjangoMixin, testlib.TestCase):
    maxDiff = None

    def test_django_db(self):
        import django.db
        module_finder = mitogen.master.ModuleFinder()
        related = module_finder.find_related('django.db')
        expected = self.MODULES_EXPECTED['find_related']['django.db']
        self.assertEqual(related, expected)

    def test_django_db_models(self):
        import django.db.models
        module_finder = mitogen.master.ModuleFinder()
        related = module_finder.find_related('django.db.models')
        expected = self.MODULES_EXPECTED['find_related']['django.db.models']
        self.assertEqual(related, expected)


class DjangoFindRelatedImportsTest(DjangoMixin, testlib.TestCase):
    maxDiff = None

    def test_django_db(self):
        import django.db
        module_finder = mitogen.master.ModuleFinder()
        related = module_finder.find_related_imports('django.db')
        expected = self.MODULES_EXPECTED['find_related_imports']['django.db']
        self.assertEqual(related, expected)

    def test_django_db_models(self):
        import django.db.models
        module_finder = mitogen.master.ModuleFinder()
        related = module_finder.find_related_imports('django.db.models')
        expected = self.MODULES_EXPECTED['find_related_imports']['django.db.models']
        self.assertEqual(related, expected)
