# The C functions used by this module are in:
#   Modules/_testcapi/module.c

import unittest
import types
from test.support import import_helper, subTests, requires_gil_enabled

# Skip this test if the _testcapi module isn't available.
_testcapi = import_helper.import_module('_testcapi')


class FakeSpec:
    name = 'testmod'

DEF_SLOTS = (
    'Py_mod_name', 'Py_mod_doc', 'Py_mod_state_size', 'Py_mod_methods',
    'Py_mod_state_traverse', 'Py_mod_state_clear', 'Py_mod_state_free',
    'Py_mod_token',
)

def def_and_token(mod):
    return (
        _testcapi.pymodule_get_def(mod),
        _testcapi.pymodule_get_token(mod),
    )

class TestModFromSlotsAndSpec(unittest.TestCase):
    def test_empty(self):
        with self.assertRaises(SystemError):
            _testcapi.module_from_slots_empty(FakeSpec())

    @requires_gil_enabled("minimal slots re-enable GIL")
    def test_minimal(self):
        mod = _testcapi.module_from_slots_minimal(FakeSpec())
        self.assertIsInstance(mod, types.ModuleType)
        self.assertEqual(def_and_token(mod), (0, 0))
        self.assertEqual(mod.__name__, 'testmod')
        size = _testcapi.pymodule_get_state_size(mod)
        self.assertEqual(size, 0)

    def test_null_slots(self):
        with self.assertRaises(SystemError):
            _testcapi.module_from_slots_null(FakeSpec())

    def test_none_spec(self):
        # The spec currently must contain a name
        with self.assertRaises(AttributeError):
            _testcapi.module_from_slots_empty(None)
        with self.assertRaises(AttributeError):
            _testcapi.module_from_slots_name(None)

    def test_name(self):
        # Py_mod_name (and PyModuleDef.m_name) are currently ignored when
        # spec is given.
        # We still test that it's accepted.
        mod = _testcapi.module_from_slots_name(FakeSpec())
        self.assertIsInstance(mod, types.ModuleType)
        self.assertEqual(def_and_token(mod), (0, 0))
        self.assertEqual(mod.__name__, 'testmod')
        self.assertEqual(mod.__doc__, None)

    def test_doc(self):
        mod = _testcapi.module_from_slots_doc(FakeSpec())
        self.assertIsInstance(mod, types.ModuleType)
        self.assertEqual(def_and_token(mod), (0, 0))
        self.assertEqual(mod.__name__, 'testmod')
        self.assertEqual(mod.__doc__, 'the docstring')

    def test_size(self):
        mod = _testcapi.module_from_slots_size(FakeSpec())
        self.assertIsInstance(mod, types.ModuleType)
        self.assertEqual(def_and_token(mod), (0, 0))
        self.assertEqual(mod.__name__, 'testmod')
        self.assertEqual(mod.__doc__, None)
        size = _testcapi.pymodule_get_state_size(mod)
        self.assertEqual(size, 123)

    def test_methods(self):
        mod = _testcapi.module_from_slots_methods(FakeSpec())
        self.assertIsInstance(mod, types.ModuleType)
        self.assertEqual(def_and_token(mod), (0, 0))
        self.assertEqual(mod.__name__, 'testmod')
        self.assertEqual(mod.__doc__, None)
        self.assertEqual(mod.a_method(456), (mod, 456))

    def test_gc(self):
        mod = _testcapi.module_from_slots_gc(FakeSpec())
        self.assertIsInstance(mod, types.ModuleType)
        self.assertEqual(def_and_token(mod), (0, 0))
        self.assertEqual(mod.__name__, 'testmod')
        self.assertEqual(mod.__doc__, None)

        # Check that the requested hook functions (which module_from_slots_gc
        # stores as attributes) match what's in the module (as retrieved by
        # _testinternalcapi.module_get_gc_hooks)
        _testinternalcapi = import_helper.import_module('_testinternalcapi')
        traverse, clear, free = _testinternalcapi.module_get_gc_hooks(mod)
        self.assertEqual(traverse, mod.traverse)
        self.assertEqual(clear, mod.clear)
        self.assertEqual(free, mod.free)

    def test_token(self):
        mod = _testcapi.module_from_slots_token(FakeSpec())
        self.assertIsInstance(mod, types.ModuleType)
        self.assertEqual(def_and_token(mod), (0, _testcapi.module_test_token))
        self.assertEqual(mod.__name__, 'testmod')
        self.assertEqual(mod.__doc__, None)

    def test_exec(self):
        mod = _testcapi.module_from_slots_exec(FakeSpec())
        self.assertIsInstance(mod, types.ModuleType)
        self.assertEqual(def_and_token(mod), (0, 0))
        self.assertEqual(mod.__name__, 'testmod')
        self.assertEqual(mod.__doc__, None)
        self.assertEqual(mod.a_number, 456)

    def test_create(self):
        spec = FakeSpec()
        spec._gimme_this = "not a module object"
        mod = _testcapi.module_from_slots_create(spec)
        self.assertIsInstance(mod, str)
        self.assertEqual(mod, "not a module object")
        with self.assertRaises(TypeError):
            _testcapi.pymodule_get_def(mod),
        with self.assertRaises(TypeError):
            _testcapi.pymodule_get_token(mod)

    def test_def_slot(self):
        """Slots cannot contradict PyModuleDef fields"""
        for name in DEF_SLOTS:
            with self.subTest(name):
                spec = FakeSpec()
                spec._test_slot_id = getattr(_testcapi, name)
                with self.assertRaises(SystemError) as cm:
                    _testcapi.module_from_def_slot(spec)
                self.assertIn(name, str(cm.exception))
                self.assertIn("PyModuleDef", str(cm.exception))

    def test_def_slot_parrot(self):
        """Slots with same value as PyModuleDef fields are allowed"""
        spec = FakeSpec()
        _testcapi.module_from_def_slot_parrot(spec)

    def test_repeated_def_slot(self):
        """Slots that replace PyModuleDef fields can't be repeated"""
        for name in (*DEF_SLOTS, 'Py_mod_exec'):
            with self.subTest(name):
                spec = FakeSpec()
                spec._test_slot_id = getattr(_testcapi, name)
                with self.assertRaises(SystemError) as cm:
                    _testcapi.module_from_slots_repeat_slot(spec)
                self.assertIn(name, str(cm.exception))
                self.assertRegex(
                    str(cm.exception),
                    rf"^module( [_\w]+)? has multiple {name}( \(\d+\))? slots$")

    def test_null_def_slot(self):
        """Slots that replace PyModuleDef fields can't be NULL"""
        for name in {*DEF_SLOTS, 'Py_mod_exec'} - {'Py_mod_state_size'}:
            with self.subTest(name):
                spec = FakeSpec()
                spec._test_slot_id = getattr(_testcapi, name)
                with self.assertRaises(SystemError) as cm:
                    _testcapi.module_from_slots_null_slot(spec)
                self.assertIn(name, str(cm.exception))
                self.assertIn("NULL", str(cm.exception))

    def test_bad_abiinfo(self):
        """Slots that incompatible ABI is rejected"""
        with self.assertRaises(ImportError) as cm:
            _testcapi.module_from_bad_abiinfo(FakeSpec())

    def test_multiple_abiinfo(self):
        """Slots that Py_mod_abiinfo can be repeated"""
        mod = _testcapi.module_from_multiple_abiinfo(FakeSpec())
        self.assertEqual(mod.__name__, 'testmod')

    def test_def_multiple_exec(self):
        """PyModule_Exec runs all exec slots of PyModuleDef-defined module"""
        mod = _testcapi.module_from_def_multiple_exec(FakeSpec())
        self.assertFalse(hasattr(mod, 'a_number'))
        _testcapi.pymodule_exec(mod)
        self.assertEqual(mod.a_number, 456)
        self.assertEqual(mod.another_number, 789)
        _testcapi.pymodule_exec(mod)
        self.assertEqual(mod.a_number, 456)
        self.assertEqual(mod.another_number, -789)
        def_ptr, token = def_and_token(mod)
        self.assertEqual(def_ptr, token)

    def test_def_token(self):
        """In PyModuleDef-defined modules, the def is the token"""
        mod = _testcapi.module_from_def_multiple_exec(FakeSpec())
        def_ptr, token = def_and_token(mod)
        self.assertEqual(def_ptr, token)
        self.assertGreater(def_ptr, 0)

    @subTests('name, expected_size', [
        (__name__, 0),  # Python module
        ('_testsinglephase', -1),  # single-phase init
        ('sys', -1),
    ])
    def test_get_state_size(self, name, expected_size):
        mod = import_helper.import_module(name)
        size = _testcapi.pymodule_get_state_size(mod)
        self.assertEqual(size, expected_size)
