# Source code for evennia.contrib.base_systems.ingame_python.tests

    """
    Module containing the test cases for the in-game Python system.
    """

    from mock import Mock
    from textwrap import dedent

    from django.conf import settings
    from evennia import ScriptDB
    from evennia.commands.default.tests import BaseEvenniaCommandTest
    from evennia.objects.objects import ExitCommand
    from evennia.utils import ansi, utils
    from evennia.utils.create import create_object, create_script
    from evennia.utils.test_resources import BaseEvenniaTest
    from .commands import CmdCallback
    from .callbackhandler import CallbackHandler

    # Force settings
    settings.EVENTS_CALENDAR = "standard"

    # Constants
    OLD_EVENTS = {}

    class TestEventHandler(BaseEvenniaTest):

        """
        Test cases of the event handler to add, edit or delete events.
        """

        def setUp(self):
            """Create the event handler."""
            super().setUp()
            self.handler = create_script(
                "evennia.contrib.base_systems.ingame_python.scripts.EventHandler"
            )

            # Copy old events if necessary
            if OLD_EVENTS:
                self.handler.ndb.events = dict(OLD_EVENTS)

            # Alter typeclasses
            self.char1.swap_typeclass(
                "evennia.contrib.base_systems.ingame_python.typeclasses.EventCharacter"
            )
            self.char2.swap_typeclass(
                "evennia.contrib.base_systems.ingame_python.typeclasses.EventCharacter"
            )
            self.room1.swap_typeclass(
                "evennia.contrib.base_systems.ingame_python.typeclasses.EventRoom"
            )
            self.room2.swap_typeclass(
                "evennia.contrib.base_systems.ingame_python.typeclasses.EventRoom"
            )
            self.exit.swap_typeclass("evennia.contrib.base_systems.ingame_python.typeclasses.EventExit")

        def tearDown(self):
            """Stop the event handler."""
            OLD_EVENTS.clear()
            OLD_EVENTS.update(self.handler.ndb.events)
            self.handler.delete()
            CallbackHandler.script = None
            super().tearDown()

        def test_start(self):
            """Simply make sure the handler runs with proper initial values."""
            self.assertEqual(self.handler.db.callbacks, {})
            self.assertEqual(self.handler.db.to_valid, [])
            self.assertEqual(self.handler.db.locked, [])
            self.assertEqual(self.handler.db.tasks, {})
            self.assertIsNotNone(self.handler.ndb.events)

        def test_add_validation(self):
            """Add a callback while needing validation."""
            author = self.char1
            self.handler.add_callback(
                self.room1, "dummy", "character.db.strength = 40", author=author, valid=False
            )
            callback = self.handler.get_callbacks(self.room1).get("dummy")
            callback = callback[0]
            self.assertIsNotNone(callback)
            self.assertEqual(callback["author"], author)
            self.assertEqual(callback["valid"], False)

            # Since this callback is not valid, it should appear in 'to_valid'
            self.assertIn((self.room1, "dummy", 0), self.handler.db.to_valid)

            # Run this dummy callback (shouldn't do anything)
            self.char1.db.strength = 10
            locals = {"character": self.char1}
            self.assertTrue(self.handler.call(self.room1, "dummy", locals=locals))
            self.assertEqual(self.char1.db.strength, 10)

        def test_edit(self):
            """Test editing a callback."""
            author = self.char1
            self.handler.add_callback(
                self.room1, "dummy", "character.db.strength = 60", author=author, valid=True
            )

            # Edit it right away
            self.handler.edit_callback(
                self.room1, "dummy", 0, "character.db.strength = 65", author=self.char2, valid=True
            )

            # Check that the callback was written
            callback = self.handler.get_callbacks(self.room1).get("dummy")
            callback = callback[0]
            self.assertIsNotNone(callback)
            self.assertEqual(callback["author"], author)
            self.assertEqual(callback["valid"], True)
            self.assertEqual(callback["updated_by"], self.char2)

            # Run this dummy callback
            self.char1.db.strength = 10
            locals = {"character": self.char1}
            self.assertTrue(self.handler.call(self.room1, "dummy", locals=locals))
            self.assertEqual(self.char1.db.strength, 65)

        def test_edit_validation(self):
            """Edit a callback when validation isn't automatic."""
            author = self.char1
            self.handler.add_callback(
                self.room1, "dummy", "character.db.strength = 70", author=author, valid=True
            )

            # Edit it right away
            self.handler.edit_callback(
                self.room1, "dummy", 0, "character.db.strength = 80", author=self.char2, valid=False
            )

            # Run this dummy callback (shouldn't do anything)
            self.char1.db.strength = 10
            locals = {"character": self.char1}
            self.assertTrue(self.handler.call(self.room1, "dummy", locals=locals))
            self.assertEqual(self.char1.db.strength, 10)

        def test_del(self):
            """Try to delete a callback."""
            # Add 3 callbacks
            self.handler.add_callback(
                self.room1, "dummy", "character.db.strength = 5", author=self.char1, valid=True
            )
            self.handler.add_callback(
                self.room1, "dummy", "character.db.strength = 8", author=self.char2, valid=False
            )
            self.handler.add_callback(
                self.room1, "dummy", "character.db.strength = 9", author=self.char1, valid=True
            )

            # Note that the second callback isn't valid
            self.assertIn((self.room1, "dummy", 1), self.handler.db.to_valid)

            # Lock the third callback
            self.handler.db.locked.append((self.room1, "dummy", 2))

            # Delete the first callback
            self.handler.del_callback(self.room1, "dummy", 0)

            # The callback #1 that was to valid should be #0 now
            self.assertIn((self.room1, "dummy", 0), self.handler.db.to_valid)
            self.assertNotIn((self.room1, "dummy", 1), self.handler.db.to_valid)

            # The lock has been updated too
            self.assertIn((self.room1, "dummy", 1), self.handler.db.locked)
            self.assertNotIn((self.room1, "dummy", 2), self.handler.db.locked)

            # Now delete the first (not valid) callback
            self.handler.del_callback(self.room1, "dummy", 0)
            self.assertEqual(self.handler.db.to_valid, [])
            self.assertIn((self.room1, "dummy", 0), self.handler.db.locked)
            self.assertNotIn((self.room1, "dummy", 1), self.handler.db.locked)

            # Call the remaining callback
            self.char1.db.strength = 10
            locals = {"character": self.char1}
            self.assertTrue(self.handler.call(self.room1, "dummy", locals=locals))
            self.assertEqual(self.char1.db.strength, 9)

        def test_accept(self):
            """Accept an callback."""
            # Add 2 callbacks
            self.handler.add_callback(
                self.room1, "dummy", "character.db.strength = 5", author=self.char1, valid=True
            )
            self.handler.add_callback(
                self.room1, "dummy", "character.db.strength = 8", author=self.char2, valid=False
            )

            # Note that the second callback isn't valid
            self.assertIn((self.room1, "dummy", 1), self.handler.db.to_valid)

            # Accept the second callback
            self.handler.accept_callback(self.room1, "dummy", 1)
            callback = self.handler.get_callbacks(self.room1).get("dummy")
            callback = callback[1]
            self.assertIsNotNone(callback)
            self.assertEqual(callback["valid"], True)

            # Call the dummy callback
            self.char1.db.strength = 10
            locals = {"character": self.char1}
            self.assertTrue(self.handler.call(self.room1, "dummy", locals=locals))
            self.assertEqual(self.char1.db.strength, 8)

        def test_call(self):
            """Test to call amore complex callback."""
            self.char1.key = "one"
            self.char2.key = "two"

            # Add an callback
            code = dedent(
                """
                if character.key == "one":
                    character.db.health = 50
                else:
                    character.db.health = 0
            """.strip(
                    "\n"
                )
            )
            self.handler.add_callback(self.room1, "dummy", code, author=self.char1, valid=True)

            # Call the dummy callback
            self.assertTrue(self.handler.call(self.room1, "dummy", locals={"character": self.char1}))
            self.assertEqual(self.char1.db.health, 50)
            self.assertTrue(self.handler.call(self.room1, "dummy", locals={"character": self.char2}))
            self.assertEqual(self.char2.db.health, 0)

        def test_handler(self):
            """Test the object handler."""
            self.assertIsNotNone(self.char1.callbacks)

            # Add an callback
            callback = self.room1.callbacks.add("dummy", "pass", author=self.char1, valid=True)
            self.assertEqual(callback.obj, self.room1)
            self.assertEqual(callback.name, "dummy")
            self.assertEqual(callback.code, "pass")
            self.assertEqual(callback.author, self.char1)
            self.assertEqual(callback.valid, True)
            self.assertIn([callback], list(self.room1.callbacks.all().values()))

            # Edit this very callback
            new = self.room1.callbacks.edit(
                "dummy", 0, "character.db.say = True", author=self.char1, valid=True
            )
            self.assertIn([new], list(self.room1.callbacks.all().values()))
            self.assertNotIn([callback], list(self.room1.callbacks.all().values()))

            # Try to call this callback
            self.assertTrue(self.room1.callbacks.call("dummy", locals={"character": self.char2}))
            self.assertTrue(self.char2.db.say)

            # Delete the callback
            self.room1.callbacks.remove("dummy", 0)
            self.assertEqual(self.room1.callbacks.all(), {})

    class TestCmdCallback(BaseEvenniaCommandTest):

        """Test the @callback command."""

        def setUp(self):
            """Create the callback handler."""
            super().setUp()
            self.handler = create_script(
                "evennia.contrib.base_systems.ingame_python.scripts.EventHandler"
            )

            # Copy old events if necessary
            if OLD_EVENTS:
                self.handler.ndb.events = dict(OLD_EVENTS)

            # Alter typeclasses
            self.char1.swap_typeclass(
                "evennia.contrib.base_systems.ingame_python.typeclasses.EventCharacter"
            )
            self.char2.swap_typeclass(
                "evennia.contrib.base_systems.ingame_python.typeclasses.EventCharacter"
            )
            self.room1.swap_typeclass(
                "evennia.contrib.base_systems.ingame_python.typeclasses.EventRoom"
            )
            self.room2.swap_typeclass(
                "evennia.contrib.base_systems.ingame_python.typeclasses.EventRoom"
            )
            self.exit.swap_typeclass("evennia.contrib.base_systems.ingame_python.typeclasses.EventExit")

        def tearDown(self):
            """Stop the callback handler."""
            OLD_EVENTS.clear()
            OLD_EVENTS.update(self.handler.ndb.events)
            self.handler.delete()
            for script in ScriptDB.objects.filter(
                db_typeclass_path="evennia.contrib.base_systems.ingame_python.scripts.TimeEventScript"
            ):
                script.delete()

            CallbackHandler.script = None
            super().tearDown()

        def test_list(self):
            """Test listing callbacks with different rights."""
            table = self.call(CmdCallback(), "out")
            lines = table.splitlines()[3:-1]
            self.assertNotEqual(lines, [])

            # Check that the second column only contains 0 (0) (no callback yet)
            for line in lines:
                cols = line.split("|")
                self.assertIn(cols[2].strip(), ("0 (0)", ""))

            # Add some callback
            self.handler.add_callback(self.exit, "traverse", "pass", author=self.char1, valid=True)

            # Try to obtain more details on a specific callback on exit
            table = self.call(CmdCallback(), "out = traverse")
            lines = table.splitlines()[3:-1]
            self.assertEqual(len(lines), 1)
            line = lines[0]
            cols = line.split("|")
            self.assertIn(cols[1].strip(), ("1", ""))
            self.assertIn(cols[2].strip(), (str(self.char1), ""))
            self.assertIn(cols[-1].strip(), ("Yes", "No", ""))

            # Run the same command with char2
            # char2 shouldn't see the last column (Valid)
            table = self.call(CmdCallback(), "out = traverse", caller=self.char2)
            lines = table.splitlines()[3:-1]
            self.assertEqual(len(lines), 1)
            line = lines[0]
            cols = line.split("|")
            self.assertEqual(cols[1].strip(), "1")
            self.assertNotIn(cols[-1].strip(), ("Yes", "No"))

            # In any case, display the callback
            # The last line should be "pass" (the callback code)
            details = self.call(CmdCallback(), "out = traverse 1")
            self.assertEqual(details.splitlines()[-1], "pass")

        def test_add(self):
            """Test to add an callback."""
            self.call(CmdCallback(), "/add out = traverse")
            editor = self.char1.ndb._eveditor
            self.assertIsNotNone(editor)

            # Edit the callback
            editor.update_buffer(
                dedent(
                    """
                if character.key == "one":
                    character.msg("You can pass.")
                else:
                    character.msg("You can't pass.")
                    deny()
            """.strip(
                        "\n"
                    )
                )
            )
            editor.save_buffer()
            editor.quit()
            callback = self.exit.callbacks.get("traverse")[0]
            self.assertEqual(callback.author, self.char1)
            self.assertEqual(callback.valid, True)
            self.assertTrue(len(callback.code) > 0)

            # We're going to try the same thing but with char2
            # char2 being a player for our test, the callback won't be validated.
            self.call(CmdCallback(), "/add out = traverse", caller=self.char2)
            editor = self.char2.ndb._eveditor
            self.assertIsNotNone(editor)

            # Edit the callback
            editor.update_buffer(
                dedent(
                    """
                character.msg("No way.")
            """.strip(
                        "\n"
                    )
                )
            )
            editor.save_buffer()
            editor.quit()
            callback = self.exit.callbacks.get("traverse")[1]
            self.assertEqual(callback.author, self.char2)
            self.assertEqual(callback.valid, False)
            self.assertTrue(len(callback.code) > 0)

        def test_del(self):
            """Add and remove an callback."""
            self.handler.add_callback(self.exit, "traverse", "pass", author=self.char1, valid=True)

            # Try to delete the callback
            # char2 shouldn't be allowed to do so (that's not HIS callback)
            self.call(CmdCallback(), "/del out = traverse 1", caller=self.char2)
            self.assertTrue(len(self.handler.get_callbacks(self.exit).get("traverse", [])) == 1)

            # Now, char1 should be allowed to delete it
            self.call(CmdCallback(), "/del out = traverse 1")
            self.assertTrue(len(self.handler.get_callbacks(self.exit).get("traverse", [])) == 0)

        def test_lock(self):
            """Test the lock of multiple editing."""
            self.call(CmdCallback(), "/add here = time 8:00", caller=self.char2)
            self.assertIsNotNone(self.char2.ndb._eveditor)

            # Now ask char1 to edit
            line = self.call(CmdCallback(), "/edit here = time 1")
            self.assertIsNone(self.char1.ndb._eveditor)

            # Try to delete this callback while char2 is editing it
            line = self.call(CmdCallback(), "/del here = time 1")

        def test_accept(self):
            """Accept an callback."""
            self.call(CmdCallback(), "/add here = time 8:00", caller=self.char2)
            editor = self.char2.ndb._eveditor
            self.assertIsNotNone(editor)

            # Edit the callback
            editor.update_buffer(
                dedent(
                    """
                room.msg_contents("It's 8 PM, everybody up!")
            """.strip(
                        "\n"
                    )
                )
            )
            editor.save_buffer()
            editor.quit()
            callback = self.room1.callbacks.get("time")[0]
            self.assertEqual(callback.valid, False)

            # chars shouldn't be allowed to the callback
            self.call(CmdCallback(), "/accept here = time 1", caller=self.char2)
            callback = self.room1.callbacks.get("time")[0]
            self.assertEqual(callback.valid, False)

            # char1 will accept the callback
            self.call(CmdCallback(), "/accept here = time 1")
            callback = self.room1.callbacks.get("time")[0]
            self.assertEqual(callback.valid, True)

    class TestDefaultCallbacks(BaseEvenniaCommandTest):

        """Test the default callbacks."""

        def setUp(self):
            """Create the callback handler."""
            super().setUp()
            self.handler = create_script(
                "evennia.contrib.base_systems.ingame_python.scripts.EventHandler"
            )

            # Copy old events if necessary
            if OLD_EVENTS:
                self.handler.ndb.events = dict(OLD_EVENTS)

            # Alter typeclasses
            self.char1.swap_typeclass(
                "evennia.contrib.base_systems.ingame_python.typeclasses.EventCharacter"
            )
            self.char2.swap_typeclass(
                "evennia.contrib.base_systems.ingame_python.typeclasses.EventCharacter"
            )
            self.room1.swap_typeclass(
                "evennia.contrib.base_systems.ingame_python.typeclasses.EventRoom"
            )
            self.room2.swap_typeclass(
                "evennia.contrib.base_systems.ingame_python.typeclasses.EventRoom"
            )
            self.exit.swap_typeclass("evennia.contrib.base_systems.ingame_python.typeclasses.EventExit")

        def tearDown(self):
            """Stop the callback handler."""
            OLD_EVENTS.clear()
            OLD_EVENTS.update(self.handler.ndb.events)
            self.handler.delete()
            CallbackHandler.script = None
            super().tearDown()

        def test_exit(self):
            """Test the callbacks of an exit."""
            self.char1.key = "char1"
            code = dedent(
                """
                if character.key == "char1":
                    character.msg("You can leave.")
                else:
                    character.msg("You cannot leave.")
                    deny()
            """.strip(
                    "\n"
                )
            )
            # Enforce self.exit.destination since swapping typeclass lose it
            self.exit.destination = self.room2

            # Try the can_traverse callback
            self.handler.add_callback(self.exit, "can_traverse", code, author=self.char1, valid=True)

            # Have char1 move through the exit
            self.call(ExitCommand(), "", "You can leave.", obj=self.exit)
            self.assertIs(self.char1.location, self.room2)

            # Have char2 move through this exit
            self.call(ExitCommand(), "", "You cannot leave.", obj=self.exit, caller=self.char2)
            self.assertIs(self.char2.location, self.room1)

            # Try the traverse callback
            self.handler.del_callback(self.exit, "can_traverse", 0)
            self.handler.add_callback(
                self.exit, "traverse", "character.msg('Fine!')", author=self.char1, valid=True
            )

            # Have char2 move through the exit
            self.call(ExitCommand(), "", obj=self.exit, caller=self.char2)
            self.assertIs(self.char2.location, self.room2)
            self.handler.del_callback(self.exit, "traverse", 0)

            # Move char1 and char2 back
            self.char1.location = self.room1
            self.char2.location = self.room1

            # Test msg_arrive and msg_leave
            code = 'message = "{character} goes out."'
            self.handler.add_callback(self.exit, "msg_leave", code, author=self.char1, valid=True)

            # Have char1 move through the exit
            old_msg = self.char2.msg
            try:
                self.char2.msg = Mock()
                self.call(ExitCommand(), "", obj=self.exit)
                stored_msg = [
                    args[0] if args and args[0] else kwargs.get("text", utils.to_str(kwargs))
                    for name, args, kwargs in self.char2.msg.mock_calls
                ]
                # Get the first element of a tuple if msg received a tuple instead of a string
                stored_msg = [smsg[0] if isinstance(smsg, tuple) else smsg for smsg in stored_msg]
                returned_msg = ansi.parse_ansi("\n".join(stored_msg), strip_ansi=True)
                self.assertEqual(returned_msg, "char1 goes out.")
            finally:
                self.char2.msg = old_msg

            # Create a return exit
            back = create_object(
                "evennia.objects.objects.DefaultExit",
                key="in",
                location=self.room2,
                destination=self.room1,
            )
            code = 'message = "{character} goes in."'
            self.handler.add_callback(self.exit, "msg_arrive", code, author=self.char1, valid=True)

            # Have char1 move through the exit
            old_msg = self.char2.msg
            try:
                self.char2.msg = Mock()
                self.call(ExitCommand(), "", obj=back)
                stored_msg = [
                    args[0] if args and args[0] else kwargs.get("text", utils.to_str(kwargs))
                    for name, args, kwargs in self.char2.msg.mock_calls
                ]
                # Get the first element of a tuple if msg received a tuple instead of a string
                stored_msg = [smsg[0] if isinstance(smsg, tuple) else smsg for smsg in stored_msg]
                returned_msg = ansi.parse_ansi("\n".join(stored_msg), strip_ansi=True)
                self.assertEqual(returned_msg, "char1 goes in.")
            finally:
                self.char2.msg = old_msg