import importlib.util
import os.path
import sys
import types
import unittest
from test.support import os_helper
from test.support import import_helper
from test.support.warnings_helper import check_warnings

_testcapi = import_helper.import_module('_testcapi')
_testlimitedcapi = import_helper.import_module('_testlimitedcapi')
NULL = None


class ImportTests(unittest.TestCase):
    def test_getmagicnumber(self):
        # Test PyImport_GetMagicNumber()
        magic = _testlimitedcapi.PyImport_GetMagicNumber()
        self.assertEqual(magic,
                         int.from_bytes(importlib.util.MAGIC_NUMBER, 'little'))

    def test_getmagictag(self):
        # Test PyImport_GetMagicTag()
        tag = _testlimitedcapi.PyImport_GetMagicTag()
        self.assertEqual(tag, sys.implementation.cache_tag)

    def test_getmoduledict(self):
        # Test PyImport_GetModuleDict()
        modules = _testlimitedcapi.PyImport_GetModuleDict()
        self.assertIs(modules, sys.modules)

    def check_import_loaded_module(self, import_module):
        for name in ('os', 'sys', 'test', 'unittest'):
            with self.subTest(name=name):
                self.assertIn(name, sys.modules)
                old_module = sys.modules[name]
                module = import_module(name)
                self.assertIsInstance(module, types.ModuleType)
                self.assertIs(module, old_module)

    def check_import_fresh_module(self, import_module):
        old_modules = dict(sys.modules)
        try:
            for name in ('colorsys', 'math'):
                with self.subTest(name=name):
                    sys.modules.pop(name, None)
                    module = import_module(name)
                    self.assertIsInstance(module, types.ModuleType)
                    self.assertIs(module, sys.modules[name])
                    self.assertEqual(module.__name__, name)
        finally:
            sys.modules.clear()
            sys.modules.update(old_modules)

    def test_getmodule(self):
        # Test PyImport_GetModule()
        getmodule = _testlimitedcapi.PyImport_GetModule
        self.check_import_loaded_module(getmodule)

        nonexistent = 'nonexistent'
        self.assertNotIn(nonexistent, sys.modules)
        self.assertIs(getmodule(nonexistent), KeyError)
        self.assertIs(getmodule(''), KeyError)
        self.assertIs(getmodule(object()), KeyError)

        self.assertRaises(TypeError, getmodule, [])  # unhashable
        # CRASHES getmodule(NULL)

    def check_addmodule(self, add_module, accept_nonstr=False):
        # create a new module
        names = ['nonexistent']
        if accept_nonstr:
            names.append(b'\xff')  # non-UTF-8
            # PyImport_AddModuleObject() accepts non-string names
            names.append(tuple(['hashable non-string']))
        for name in names:
            with self.subTest(name=name):
                self.assertNotIn(name, sys.modules)
                try:
                    module = add_module(name)
                    self.assertIsInstance(module, types.ModuleType)
                    self.assertEqual(module.__name__, name)
                    self.assertIs(module, sys.modules[name])
                finally:
                    sys.modules.pop(name, None)

        # get an existing module
        self.check_import_loaded_module(add_module)

    def test_addmoduleobject(self):
        # Test PyImport_AddModuleObject()
        addmoduleobject = _testlimitedcapi.PyImport_AddModuleObject
        self.check_addmodule(addmoduleobject, accept_nonstr=True)

        self.assertRaises(TypeError, addmoduleobject, [])  # unhashable
        # CRASHES addmoduleobject(NULL)

    def test_addmodule(self):
        # Test PyImport_AddModule()
        addmodule = _testlimitedcapi.PyImport_AddModule
        self.check_addmodule(addmodule)

        self.assertRaises(UnicodeDecodeError, addmodule, b'\xff')
        # CRASHES addmodule(NULL)

    def test_addmoduleref(self):
        # Test PyImport_AddModuleRef()
        addmoduleref = _testlimitedcapi.PyImport_AddModuleRef
        self.check_addmodule(addmoduleref)

        self.assertRaises(UnicodeDecodeError, addmoduleref, b'\xff')
        # CRASHES addmoduleref(NULL)

    def check_import_func(self, import_module):
        self.check_import_loaded_module(import_module)
        self.check_import_fresh_module(import_module)
        self.assertRaises(ModuleNotFoundError, import_module, 'nonexistent')
        self.assertRaises(ValueError, import_module, '')

    def test_import(self):
        # Test PyImport_Import()
        import_ = _testlimitedcapi.PyImport_Import
        self.check_import_func(import_)

        self.assertRaises(TypeError, import_, b'os')
        self.assertRaises(SystemError, import_, NULL)

    def test_importmodule(self):
        # Test PyImport_ImportModule()
        importmodule = _testlimitedcapi.PyImport_ImportModule
        self.check_import_func(importmodule)

        self.assertRaises(UnicodeDecodeError, importmodule, b'\xff')
        # CRASHES importmodule(NULL)

    def test_importmodulenoblock(self):
        # Test deprecated (stable ABI only) PyImport_ImportModuleNoBlock()
        importmodulenoblock = _testlimitedcapi.PyImport_ImportModuleNoBlock
        with check_warnings(('', DeprecationWarning)):
            self.check_import_func(importmodulenoblock)
            self.assertRaises(UnicodeDecodeError, importmodulenoblock, b'\xff')

        # CRASHES importmodulenoblock(NULL)

    def check_frozen_import(self, import_frozen_module):
        # Importing a frozen module executes its code, so start by unloading
        # the module to execute the code in a new (temporary) module.
        old_zipimport = sys.modules.pop('zipimport')
        try:
            self.assertEqual(import_frozen_module('zipimport'), 1)

            # import zipimport again
            self.assertEqual(import_frozen_module('zipimport'), 1)
        finally:
            sys.modules['zipimport'] = old_zipimport

        # not a frozen module
        self.assertEqual(import_frozen_module('sys'), 0)
        self.assertEqual(import_frozen_module('nonexistent'), 0)
        self.assertEqual(import_frozen_module(''), 0)

    def test_importfrozenmodule(self):
        # Test PyImport_ImportFrozenModule()
        importfrozenmodule = _testlimitedcapi.PyImport_ImportFrozenModule
        self.check_frozen_import(importfrozenmodule)

        self.assertRaises(UnicodeDecodeError, importfrozenmodule, b'\xff')
        # CRASHES importfrozenmodule(NULL)

    def test_importfrozenmoduleobject(self):
        # Test PyImport_ImportFrozenModuleObject()
        importfrozenmoduleobject = _testlimitedcapi.PyImport_ImportFrozenModuleObject
        self.check_frozen_import(importfrozenmoduleobject)
        self.assertEqual(importfrozenmoduleobject(b'zipimport'), 0)
        self.assertEqual(importfrozenmoduleobject(NULL), 0)

    def test_importmoduleex(self):
        # Test PyImport_ImportModuleEx()
        importmoduleex = _testlimitedcapi.PyImport_ImportModuleEx
        self.check_import_func(lambda name: importmoduleex(name, NULL, NULL, NULL))

        self.assertRaises(ModuleNotFoundError, importmoduleex, 'nonexistent', NULL, NULL, NULL)
        self.assertRaises(ValueError, importmoduleex, '', NULL, NULL, NULL)
        self.assertRaises(UnicodeDecodeError, importmoduleex, b'\xff', NULL, NULL, NULL)
        # CRASHES importmoduleex(NULL, NULL, NULL, NULL)

    def check_importmodulelevel(self, importmodulelevel):
        self.check_import_func(lambda name: importmodulelevel(name, NULL, NULL, NULL, 0))

        self.assertRaises(ModuleNotFoundError, importmodulelevel, 'nonexistent', NULL, NULL, NULL, 0)
        self.assertRaises(ValueError, importmodulelevel, '', NULL, NULL, NULL, 0)

        if __package__:
            self.assertIs(importmodulelevel('test_import', globals(), NULL, NULL, 1),
                          sys.modules['test.test_capi.test_import'])
            self.assertIs(importmodulelevel('test_capi', globals(), NULL, NULL, 2),
                          sys.modules['test.test_capi'])
        self.assertRaises(ValueError, importmodulelevel, 'os', NULL, NULL, NULL, -1)
        with self.assertWarns(ImportWarning):
            self.assertRaises(KeyError, importmodulelevel, 'test_import', {}, NULL, NULL, 1)
        self.assertRaises(TypeError, importmodulelevel, 'test_import', [], NULL, NULL, 1)

    def test_importmodulelevel(self):
        # Test PyImport_ImportModuleLevel()
        importmodulelevel = _testlimitedcapi.PyImport_ImportModuleLevel
        self.check_importmodulelevel(importmodulelevel)

        self.assertRaises(UnicodeDecodeError, importmodulelevel, b'\xff', NULL, NULL, NULL, 0)
        # CRASHES importmodulelevel(NULL, NULL, NULL, NULL, 0)

    def test_importmodulelevelobject(self):
        # Test PyImport_ImportModuleLevelObject()
        importmodulelevel = _testlimitedcapi.PyImport_ImportModuleLevelObject
        self.check_importmodulelevel(importmodulelevel)

        self.assertRaises(TypeError, importmodulelevel, b'os', NULL, NULL, NULL, 0)
        self.assertRaises(ValueError, importmodulelevel, NULL, NULL, NULL, NULL, 0)

    def check_executecodemodule(self, execute_code, *args):
        name = 'test_import_executecode'
        try:
            # Create a temporary module where the code will be executed
            self.assertNotIn(name, sys.modules)
            module = _testlimitedcapi.PyImport_AddModuleRef(name)
            self.assertNotHasAttr(module, 'attr')

            # Execute the code
            code = compile('attr = 1', '<test>', 'exec')
            module2 = execute_code(name, code, *args)
            self.assertIs(module2, module)

            # Check the function side effects
            self.assertEqual(module.attr, 1)
        finally:
            sys.modules.pop(name, None)
        return module.__spec__.origin

    def test_executecodemodule(self):
        # Test PyImport_ExecCodeModule()
        execcodemodule = _testlimitedcapi.PyImport_ExecCodeModule
        self.check_executecodemodule(execcodemodule)

        code = compile('attr = 1', '<test>', 'exec')
        self.assertRaises(UnicodeDecodeError, execcodemodule, b'\xff', code)
        # CRASHES execcodemodule(NULL, code)
        # CRASHES execcodemodule(name, NULL)

    def test_executecodemoduleex(self):
        # Test PyImport_ExecCodeModuleEx()
        execcodemoduleex = _testlimitedcapi.PyImport_ExecCodeModuleEx

        # Test NULL path (it should not crash)
        self.check_executecodemodule(execcodemoduleex, NULL)

        # Test non-NULL path
        pathname = b'pathname'
        origin = self.check_executecodemodule(execcodemoduleex, pathname)
        self.assertEqual(origin, os.path.abspath(os.fsdecode(pathname)))

        pathname = os_helper.TESTFN_UNDECODABLE
        if pathname:
            origin = self.check_executecodemodule(execcodemoduleex, pathname)
            self.assertEqual(origin, os.path.abspath(os.fsdecode(pathname)))

        code = compile('attr = 1', '<test>', 'exec')
        self.assertRaises(UnicodeDecodeError, execcodemoduleex, b'\xff', code, NULL)
        # CRASHES execcodemoduleex(NULL, code, NULL)
        # CRASHES execcodemoduleex(name, NULL, NULL)

    def check_executecode_pathnames(self, execute_code_func, object=False):
        # Test non-NULL pathname and NULL cpathname

        # Test NULL paths (it should not crash)
        self.check_executecodemodule(execute_code_func, NULL, NULL)

        pathname = 'pathname'
        origin = self.check_executecodemodule(execute_code_func, pathname, NULL)
        self.assertEqual(origin, os.path.abspath(os.fsdecode(pathname)))
        origin = self.check_executecodemodule(execute_code_func, NULL, pathname)
        if not object:
            self.assertEqual(origin, os.path.abspath(os.fsdecode(pathname)))

        pathname = os_helper.TESTFN_UNDECODABLE
        if pathname:
            if object:
                pathname = os.fsdecode(pathname)
            origin = self.check_executecodemodule(execute_code_func, pathname, NULL)
            self.assertEqual(origin, os.path.abspath(os.fsdecode(pathname)))
            self.check_executecodemodule(execute_code_func, NULL, pathname)

        # Test NULL pathname and non-NULL cpathname
        try:
            pyc_filename = importlib.util.cache_from_source(__file__)
        except NotImplementedError:
            return
        py_filename = importlib.util.source_from_cache(pyc_filename)
        origin = self.check_executecodemodule(execute_code_func, NULL, pyc_filename)
        if not object:
            self.assertEqual(origin, py_filename)

    def test_executecodemodulewithpathnames(self):
        # Test PyImport_ExecCodeModuleWithPathnames()
        execute_code_func = _testlimitedcapi.PyImport_ExecCodeModuleWithPathnames
        self.check_executecode_pathnames(execute_code_func)

        code = compile('attr = 1', '<test>', 'exec')
        self.assertRaises(UnicodeDecodeError, execute_code_func, b'\xff', code, NULL, NULL)
        # CRASHES execute_code_func(NULL, code, NULL, NULL)
        # CRASHES execute_code_func(name, NULL, NULL, NULL)

    def test_executecodemoduleobject(self):
        # Test PyImport_ExecCodeModuleObject()
        execute_code_func = _testlimitedcapi.PyImport_ExecCodeModuleObject
        self.check_executecode_pathnames(execute_code_func, object=True)

        code = compile('attr = 1', '<test>', 'exec')
        self.assertRaises(TypeError, execute_code_func, [], code, NULL, NULL)
        nonstring = tuple(['hashable non-string'])
        self.assertRaises(AttributeError, execute_code_func, nonstring, code, NULL, NULL)
        sys.modules.pop(nonstring, None)
        # CRASHES execute_code_func(NULL, code, NULL, NULL)
        # CRASHES execute_code_func(name, NULL, NULL, NULL)

    def check_importmoduleattr(self, importmoduleattr):
        self.assertIs(importmoduleattr('sys', 'argv'), sys.argv)
        self.assertIs(importmoduleattr('types', 'ModuleType'), types.ModuleType)

        # module name containing a dot
        attr = importmoduleattr('email.message', 'Message')
        from email.message import Message
        self.assertIs(attr, Message)

        with self.assertRaises(ImportError):
            # nonexistent module
            importmoduleattr('nonexistentmodule', 'attr')
        with self.assertRaises(AttributeError):
            # nonexistent attribute
            importmoduleattr('sys', 'nonexistentattr')
        with self.assertRaises(AttributeError):
            # attribute name containing a dot
            importmoduleattr('sys', 'implementation.name')

    def test_importmoduleattr(self):
        # Test PyImport_ImportModuleAttr()
        importmoduleattr = _testcapi.PyImport_ImportModuleAttr
        self.check_importmoduleattr(importmoduleattr)

        # Invalid module name type
        for mod_name in (object(), 123, b'bytes'):
            with self.subTest(mod_name=mod_name):
                with self.assertRaises(TypeError):
                    importmoduleattr(mod_name, "attr")

        # Invalid attribute name type
        for attr_name in (object(), 123, b'bytes'):
            with self.subTest(attr_name=attr_name):
                with self.assertRaises(TypeError):
                    importmoduleattr("sys", attr_name)

        with self.assertRaises(SystemError):
            importmoduleattr(NULL, "argv")
        # CRASHES importmoduleattr("sys", NULL)

    def test_importmoduleattrstring(self):
        # Test PyImport_ImportModuleAttrString()
        importmoduleattr = _testcapi.PyImport_ImportModuleAttrString
        self.check_importmoduleattr(importmoduleattr)

        with self.assertRaises(UnicodeDecodeError):
            importmoduleattr(b"sys\xff", "argv")
        with self.assertRaises(UnicodeDecodeError):
            importmoduleattr("sys", b"argv\xff")

        # CRASHES importmoduleattr(NULL, "argv")
        # CRASHES importmoduleattr("sys", NULL)

    # TODO: test PyImport_GetImporter()
    # TODO: test PyImport_ReloadModule()
    # TODO: test PyImport_ExtendInittab()
    # PyImport_AppendInittab() is tested by test_embed


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