# Source code for evennia.contrib.rpg.buffs.tests

    """
    Tests for the buff system contrib
    """
    from unittest.mock import Mock, call, patch
    from evennia import DefaultObject, create_object
    from evennia.utils import create
    from evennia.utils.utils import lazy_property
    from .samplebuffs import StatBuff
    from .buff import BaseBuff, Mod, BuffHandler, BuffableProperty
    from evennia.utils.test_resources import EvenniaTest

    from evennia.contrib.rpg.buffs import buff

    class _EmptyBuff(BaseBuff):
        key = "empty"
        pass

    class _TestDivBuff(BaseBuff):
        key = "tdb"
        name = "tdb"
        flavor = "divverbuff"
        mods = [Mod("stat1", "div", 1)]

    class _TestModBuff(BaseBuff):
        key = "tmb"
        name = "tmb"
        flavor = "modderbuff"
        maxstacks = 5
        mods = [Mod("stat1", "add", 10, 5), Mod("stat2", "mult", 0.5)]

    class _TestModBuff2(BaseBuff):
        key = "tmb2"
        name = "tmb2"
        flavor = "modderbuff2"
        maxstacks = 1
        mods = [Mod("stat1", "mult", 1.0), Mod("stat1", "add", 10)]

    class _TestTrigBuff(BaseBuff):
        key = "ttb"
        name = "ttb"
        flavor = "triggerbuff"
        triggers = ["test1", "test2"]

        def at_trigger(self, trigger: str, *args, **kwargs):
            if trigger == "test1":
                self.owner.db.triggertest1 = True
            if trigger == "test2":
                self.owner.db.triggertest2 = True

    class _TestConBuff(BaseBuff):
        key = "tcb"
        name = "tcb"
        flavor = "condbuff"
        triggers = ["condtest"]

        def conditional(self, *args, **kwargs):
            return self.owner.db.cond1

        def at_trigger(self, trigger: str, attacker=None, defender=None, damage=0, *args, **kwargs):
            defender.db.att, defender.db.dmg = attacker, damage

    class _TestComplexBuff(BaseBuff):
        key = "tcomb"
        name = "complex"
        flavor = "combuff"
        triggers = ["comtest", "complextest"]

        mods = [
            Mod("com1", "add", 0, 10),
            Mod("com1", "add", 15),
            Mod("com1", "mult", 2.0),
            Mod("com2", "add", 100),
        ]

        def conditional(self, cond=False, *args, **kwargs):
            return not cond

        def at_trigger(self, trigger: str, *args, **kwargs):
            if trigger == "comtest":
                self.owner.db.comtext = {"cond": True}
            else:
                self.owner.db.comtext = {}

    class _TestTimeBuff(BaseBuff):
        key = "ttib"
        name = "ttib"
        flavor = "timerbuff"
        maxstacks = 1
        tickrate = 1
        duration = 5
        mods = [Mod("timetest", "add", 665)]

        def at_tick(self, initial=True, *args, **kwargs):
            self.owner.db.ticktest = True

    class BuffableObject(DefaultObject):
        stat1 = BuffableProperty(10)

        @lazy_property
        def buffs(self) -> BuffHandler:
            return BuffHandler(self)

        def at_init(self):
            self.stat1, self.buffs
            return super().at_init()

    class TestBuffsAndHandler(EvenniaTest):
        "This tests a number of things about buffs."

        def setUp(self):
            super().setUp()
            self.testobj = create.create_object(BuffableObject, key="testobj")

        def tearDown(self):
            """done after every test_* method below"""
            self.testobj.buffs.clear()
            del self.testobj
            super().tearDown()

        @patch("evennia.contrib.rpg.buffs.buff.utils.delay", new=Mock())
        def test_addremove(self):
            """tests adding and removing buffs"""
            # setup
            handler: BuffHandler = self.testobj.buffs
            # add
            handler.add(_TestModBuff, to_cache={"cachetest": True})
            handler.add(_TestTrigBuff)
            self.assertEqual(self.testobj.db.buffs["tmb"]["ref"], _TestModBuff)
            self.assertTrue(self.testobj.db.buffs["tmb"].get("cachetest"))
            self.assertFalse(self.testobj.db.buffs["ttb"].get("cachetest"))
            # has
            self.assertTrue(handler.has(_TestModBuff))
            self.assertTrue(handler.has("tmb"))
            self.assertFalse(handler.has(_EmptyBuff))
            # remove
            handler.remove("tmb")
            self.assertFalse(self.testobj.db.buffs.get("tmb"))
            # remove stacks
            handler.add(_TestModBuff, stacks=3)
            handler.remove("tmb", stacks=3)
            self.assertFalse(self.testobj.db.buffs.get("tmb"))
            # remove by type
            handler.add(_TestModBuff)
            handler.remove_by_type(_TestModBuff)
            self.assertFalse(self.testobj.db.buffs.get("tmb"))
            # remove by buff instance
            handler.add(_TestModBuff)
            handler.all["tmb"].remove()
            self.assertFalse(self.testobj.db.buffs.get("tmb"))
            # remove by source
            handler.add(_TestModBuff)
            handler.remove_by_source(None)
            self.assertFalse(self.testobj.db.buffs.get("tmb"))
            # remove by cachevalue
            handler.add(_TestModBuff)
            handler.remove_by_cachevalue("failure", True)
            self.assertTrue(self.testobj.db.buffs.get("tmb"))
            # remove all
            handler.add(_TestModBuff)
            handler.clear()
            self.assertFalse(self.testobj.buffs.all)

        @patch("evennia.contrib.rpg.buffs.buff.utils.delay", new=Mock())
        def test_getters(self):
            """tests all built-in getters"""
            # setup
            handler: BuffHandler = self.testobj.buffs
            handler.add(_TestModBuff, source=self.obj2)
            handler.add(_TestTrigBuff, to_cache={"ttbcache": True})
            # normal getter
            self.assertTrue(isinstance(handler.get("tmb"), _TestModBuff))
            # stat getters
            self.assertTrue(isinstance(handler.get_by_stat("stat1")["tmb"], _TestModBuff))
            self.assertFalse(handler.get_by_stat("nullstat"))
            # trigger getters
            self.assertTrue("ttb" in handler.get_by_trigger("test1").keys())
            self.assertFalse("ttb" in handler.get_by_trigger("nulltrig").keys())
            # type getters
            self.assertTrue("tmb" in handler.get_by_type(_TestModBuff))
            self.assertFalse("tmb" in handler.get_by_type(_EmptyBuff))
            # source getter
            self.assertTrue("tmb" in handler.get_by_source(self.obj2))
            self.assertFalse("ttb" in handler.get_by_source(self.obj2))
            # cachevalue getter
            self.assertFalse("tmb" in handler.get_by_cachevalue("ttbcache"))
            self.assertTrue("ttb" in handler.get_by_cachevalue("ttbcache"))
            self.assertTrue("ttb" in handler.get_by_cachevalue("ttbcache", True))

        @patch("evennia.contrib.rpg.buffs.buff.utils.delay", new=Mock())
        def test_details(self):
            """tests that buff details like name and flavor are correct; also test modifier viewing"""
            handler: BuffHandler = self.testobj.buffs
            handler.add(_TestModBuff)
            handler.add(_TestTrigBuff)
            self.assertEqual(handler.get("tmb").flavor, "modderbuff")
            self.assertEqual(handler.get("ttb").name, "ttb")
            mods = handler.view_modifiers("stat1")
            _testmods = {
                "add": {"total": 15, "strongest": 15},
                "mult": {"total": 0, "strongest": 0},
                "div": {"total": 0, "strongest": 0},
            }
            self.assertDictEqual(mods, _testmods)

        @patch("evennia.contrib.rpg.buffs.buff.utils.delay", new=Mock())
        def test_modify(self):
            """tests to ensure that values are modified correctly, and stack across mods"""
            # setup
            handler: BuffHandler = self.testobj.buffs
            _stat1, _stat2 = 0, 10
            handler.add(_TestModBuff)
            # stat1 and 2 basic mods
            self.assertEqual(handler.check(_stat1, "stat1"), 15)
            self.assertEqual(handler.check(_stat2, "stat2"), 15)
            # checks can take any base value
            self.assertEqual(handler.check(_stat1, "stat2"), 0)
            self.assertEqual(handler.check(_stat2, "stat1"), 25)
            # change to base stat reflected in check
            _stat1 += 5
            self.assertEqual(handler.check(_stat1, "stat1"), 20)
            _stat2 += 10
            self.assertEqual(handler.check(_stat2, "stat2"), 30)
            # test stacking; single stack, multiple stack, max stacks
            handler.add(_TestModBuff)
            self.assertEqual(handler.check(_stat1, "stat1"), 25)
            handler.add(_TestModBuff, stacks=3)
            self.assertEqual(handler.check(_stat1, "stat1"), 40)
            handler.add(_TestModBuff, stacks=5)
            self.assertEqual(handler.check(_stat1, "stat1"), 40)
            # stat2 mod doesn't stack
            self.assertEqual(handler.check(_stat2, "stat2"), 30)
            # layers with second mod
            handler.add(_TestModBuff2)
            self.assertEqual(handler.check(_stat1, "stat1"), 100)
            self.assertEqual(handler.check(_stat2, "stat2"), 30)
            # apply only the strongest value
            self.assertEqual(handler.check(_stat1, "stat1", strongest=True), 80)
            # removing mod properly reduces value, doesn't affect other mods
            handler.remove_by_type(_TestModBuff)
            self.assertEqual(handler.check(_stat1, "stat1"), 30)
            self.assertEqual(handler.check(_stat2, "stat2"), 20)
            # divider mod test
            handler.add(_TestDivBuff)
            self.assertEqual(handler.check(_stat1, "stat1"), 15)

        @patch("evennia.contrib.rpg.buffs.buff.utils.delay", new=Mock())
        def test_trigger(self):
            """tests to ensure triggers correctly fire"""
            # setup
            handler: BuffHandler = self.testobj.buffs
            handler.add(_TestTrigBuff)
            # trigger buffs
            handler.trigger("nulltest")
            self.assertFalse(self.testobj.db.triggertest1)
            self.assertFalse(self.testobj.db.triggertest2)
            handler.trigger("test1")
            self.assertTrue(self.testobj.db.triggertest1)
            self.assertFalse(self.testobj.db.triggertest2)
            handler.trigger("test2")
            self.assertTrue(self.testobj.db.triggertest1)
            self.assertTrue(self.testobj.db.triggertest2)

        @patch("evennia.contrib.rpg.buffs.buff.utils.delay", new=Mock())
        def test_context_conditional(self):
            """tests to ensure context is passed to buffs, and also tests conditionals"""
            # setup
            handler: BuffHandler = self.testobj.buffs
            handler.add(_TestConBuff)
            self.testobj.db.cond1, self.testobj.db.att, self.testobj.db.dmg = False, None, 0
            # context to pass, containing basic event data and a little extra to be ignored
            _testcontext = {
                "attacker": self.obj2,
                "defender": self.testobj,
                "damage": 5,
                "overflow": 10,
            }
            # test negative conditional
            self.assertEqual(
                handler.get_by_type(_TestConBuff)["tcb"].conditional(**_testcontext), False
            )
            handler.trigger("condtest", _testcontext)
            self.assertEqual(self.testobj.db.att, None)
            self.assertEqual(self.testobj.db.dmg, 0)
            # test positive conditional + context passing
            self.testobj.db.cond1 = True
            self.assertEqual(handler.get_by_type(_TestConBuff)["tcb"].conditional(**_testcontext), True)
            handler.trigger("condtest", _testcontext)
            self.assertEqual(self.testobj.db.att, self.obj2)
            self.assertEqual(self.testobj.db.dmg, 5)

        @patch("evennia.contrib.rpg.buffs.buff.utils.delay", new=Mock())
        def test_complex(self):
            """tests a complex mod (conditionals, multiple triggers/mods)"""
            # setup
            handler: BuffHandler = self.testobj.buffs
            self.testobj.db.comone, self.testobj.db.comtwo, self.testobj.db.comtext = 10, 0, {}
            handler.add(_TestComplexBuff)
            # stat checks work correctly and separately
            self.assertEqual(self.testobj.db.comtext, {})
            self.assertEqual(handler.check(self.testobj.db.comone, "com1"), 105)
            self.assertEqual(handler.check(self.testobj.db.comtwo, "com2"), 100)
            # stat checks don't happen if the conditional is true
            handler.trigger("comtest", self.testobj.db.comtext)
            self.assertEqual(self.testobj.db.comtext, {"cond": True})
            self.assertEqual(
                handler.get_by_type(_TestComplexBuff)["tcomb"].conditional(**self.testobj.db.comtext),
                False,
            )
            self.assertEqual(
                handler.check(self.testobj.db.comone, "com1", context=self.testobj.db.comtext), 10
            )
            self.assertEqual(
                handler.check(self.testobj.db.comtwo, "com2", context=self.testobj.db.comtext), 0
            )
            handler.trigger("complextest", self.testobj.db.comtext)
            self.assertEqual(
                handler.check(self.testobj.db.comone, "com1", context=self.testobj.db.comtext), 10
            )
            self.assertEqual(
                handler.check(self.testobj.db.comtwo, "com2", context=self.testobj.db.comtext), 0
            )
            # separate trigger follows different codepath
            self.testobj.db.comtext = {"cond": False}
            handler.trigger("complextest", self.testobj.db.comtext)
            self.assertEqual(self.testobj.db.comtext, {})
            self.assertEqual(
                handler.check(self.testobj.db.comone, "com1", context=self.testobj.db.comtext), 105
            )
            self.assertEqual(
                handler.check(self.testobj.db.comtwo, "com2", context=self.testobj.db.comtext), 100
            )

        @patch("evennia.contrib.rpg.buffs.buff.utils.delay")
        def test_timing(self, mock_delay: Mock):
            """tests timing-related features, such as ticking and duration"""
            # setup
            handler: BuffHandler = self.testobj.buffs
            mock_delay.side_effect = [None, handler.cleanup]
            handler.add(_TestTimeBuff)
            calls = [
                call(
                    1,
                    buff.tick_buff,
                    handler=handler,
                    buffkey="ttib",
                    context={},
                    initial=False,
                    persistent=True,
                ),
                call(5, handler.cleanup, persistent=True),
            ]
            mock_delay.assert_has_calls(calls)
            self.testobj.db.timetest, self.testobj.db.ticktest = 1, False
            # test duration and ticking
            _instance = handler.get("ttib")
            self.assertTrue(_instance.ticking)
            self.assertEqual(_instance.duration, 5)
            _instance.at_tick()
            self.assertTrue(self.testobj.db.ticktest)
            # test duration modification and cleanup
            _instance.duration = 0
            self.assertEqual(handler.get("ttib").duration, 0)
            handler.cleanup()
            self.assertFalse(handler.get("ttib"), None)

        @patch("evennia.contrib.rpg.buffs.buff.utils.delay", new=Mock())
        def test_cacheattrlink(self):
            """tests the link between the instance attribute and the cache attribute"""
            # setup
            handler: BuffHandler = self.testobj.buffs
            handler.add(_EmptyBuff)
            self.assertEqual(handler.buffcache["empty"]["duration"], -1)
            empty: _EmptyBuff = handler.get("empty")
            empty.duration = 30
            self.assertEqual(handler.buffcache["empty"]["duration"], 30)

        @patch("evennia.contrib.rpg.buffs.buff.utils.delay", new=Mock())
        def test_buffableproperty(self):
            """tests buffable properties"""
            # setup
            self.testobj.buffs.add(_TestModBuff)
            self.assertEqual(self.testobj.stat1, 25)
            self.testobj.buffs.remove("tmb")
            self.assertEqual(self.testobj.stat1, 10)

        @patch("evennia.contrib.rpg.buffs.buff.utils.delay", new=Mock())
        def test_stresstest(self):
            """tests large amounts of buffs, and related removal methods"""
            # setup
            for x in range(1, 20):
                self.testobj.buffs.add(_TestModBuff, key="test" + str(x))
                self.testobj.buffs.add(_TestTrigBuff, key="trig" + str(x))
            self.assertEqual(self.testobj.stat1, 295)
            self.testobj.buffs.trigger("test1")
            self.testobj.buffs.remove_by_type(_TestModBuff)
            self.assertEqual(self.testobj.stat1, 10)
            self.testobj.buffs.clear()
            self.assertFalse(self.testobj.buffs.all)

        @patch("evennia.contrib.rpg.buffs.buff.utils.delay", new=Mock())
        def test_modgen(self):
            """test generating mods on the fly"""
            # setup
            handler: BuffHandler = self.testobj.buffs
            self.testobj.db.gentest = 5
            self.assertEqual(self.testobj.db.gentest, 5)
            tc = {"modgen": ["gentest", "add", 5, 0]}
            handler.add(StatBuff, key="gentest", to_cache=tc)
            self.assertEqual(handler.check(self.testobj.db.gentest, "gentest"), 10)
            tc = {"modgen": ["gentest", "add", 10, 0]}
            handler.add(StatBuff, key="gentest", to_cache=tc)
            self.assertEqual(handler.check(self.testobj.db.gentest, "gentest"), 15)
            self.assertEqual(
                handler.get("gentest").flavor, "This buff affects the following stats: gentest"
            )