#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
Unit test module for Trait classes.

"""

from copy import copy

from anything import Something
from evennia.objects.objects import DefaultCharacter
from evennia.utils.test_resources import BaseEvenniaTestCase, EvenniaTest
from mock import MagicMock, patch

from . import traits


class _MockObj:
    def __init__(self):
        self.attributes = MagicMock()
        self.attributes.get = self.get
        self.attributes.add = self.add
        self.dbstore = {}
        self.category = "traits"

    def get(self, key, category=None):
        assert category == self.category
        return self.dbstore.get(key)

    def add(self, key, value, category=None):
        assert category == self.category
        self.dbstore[key] = value


# we want to test the base traits too
_TEST_TRAIT_CLASS_PATHS = [
    "evennia.contrib.rpg.traits.Trait",
    "evennia.contrib.rpg.traits.StaticTrait",
    "evennia.contrib.rpg.traits.CounterTrait",
    "evennia.contrib.rpg.traits.GaugeTrait",
]


class _TraitHandlerBase(BaseEvenniaTestCase):
    "Base for trait tests"

    @patch("evennia.contrib.rpg.traits.traits._TRAIT_CLASS_PATHS", new=_TEST_TRAIT_CLASS_PATHS)
    def setUp(self):
        self.obj = _MockObj()
        self.traithandler = traits.TraitHandler(self.obj)
        self.obj.traits = self.traithandler

    def _get_dbstore(self, key):
        return self.obj.dbstore["traits"][key]


class TraitHandlerTest(_TraitHandlerBase):
    """Testing for TraitHandler"""

    def setUp(self):
        super().setUp()
        self.traithandler.add("test1", name="Test1", trait_type="trait")
        self.traithandler.add(
            "test2",
            name="Test2",
            trait_type="trait",
            value=["foo", {"1": [1, 2, 3]}, 4],
        )

    def test_add_trait(self):
        self.assertEqual(
            self._get_dbstore("test1"),
            {
                "name": "Test1",
                "trait_type": "trait",
                "value": None,
            },
        )
        self.assertEqual(
            self._get_dbstore("test2"),
            {
                "name": "Test2",
                "trait_type": "trait",
                "value": ["foo", {"1": [1, 2, 3]}, 4],
            },
        )
        self.assertEqual(len(self.traithandler), 2)

    def test_cache(self):
        """
        Cache should not be set until first get
        """
        self.assertEqual(len(self.traithandler._cache), 0)
        self.traithandler.all()  # does not affect cache
        self.assertEqual(len(self.traithandler._cache), 0)
        self.traithandler.test1
        self.assertEqual(len(self.traithandler._cache), 1)
        self.traithandler.test2
        self.assertEqual(len(self.traithandler._cache), 2)

    def test_setting(self):
        "Don't allow setting stuff on traithandler"
        with self.assertRaises(traits.TraitException):
            self.traithandler.foo = "bar"
        with self.assertRaises(traits.TraitException):
            self.traithandler["foo"] = "bar"
        with self.assertRaises(traits.TraitException):
            self.traithandler.test1 = "foo"

    def test_getting(self):
        "Test we are getting data from the dbstore"
        self.assertEqual(
            self.traithandler.test1._data, {"name": "Test1", "trait_type": "trait", "value": None}
        )
        self.assertEqual(self.traithandler._cache, Something)
        self.assertEqual(
            self.traithandler.test2._data,
            {"name": "Test2", "trait_type": "trait", "value": ["foo", {"1": [1, 2, 3]}, 4]},
        )
        self.assertEqual(self.traithandler._cache, Something)
        self.assertFalse(self.traithandler.get("foo"))
        self.assertFalse(self.traithandler.bar)

    def test_all(self):
        "Test all method"
        self.assertEqual(self.traithandler.all(), ["test1", "test2"])

    def test_remove(self):
        "Test remove method"
        self.traithandler.remove("test2")
        self.assertEqual(len(self.traithandler), 1)
        self.assertTrue(bool(self.traithandler.get("test1")))  # this populates cache
        self.assertEqual(len(self.traithandler._cache), 1)
        with self.assertRaises(traits.TraitException):
            self.traithandler.remove("foo")

    def test_clear(self):
        "Test clear method"
        self.traithandler.clear()
        self.assertEqual(len(self.traithandler), 0)

    def test_trait_db_connection(self):
        "Test that updating a trait property actually updates value in db"
        trait = self.traithandler.test1
        self.assertEqual(trait.value, None)
        trait.value = 10
        self.assertEqual(trait.value, 10)
        self.assertEqual(self.obj.attributes.get("traits", category="traits")["test1"]["value"], 10)
        trait.value = 20
        self.assertEqual(trait.value, 20)
        self.assertEqual(self.obj.attributes.get("traits", category="traits")["test1"]["value"], 20)
        del trait.value
        self.assertEqual(
            self.obj.attributes.get("traits", category="traits")["test1"]["value"], None
        )

    def test_related_traits(self):
        """Test traits related to each other via Trait.get_trait()"""

        trait1 = self.traithandler.test1
        trait2 = self.traithandler.test2

        self.assertEqual(trait1.traithandler, self.traithandler)
        self.assertEqual(trait1.get_trait("test1"), trait1)
        self.assertEqual(trait1.get_trait("test2"), trait2)


class TestTrait(_TraitHandlerBase):
    """
    Test the base Trait class
    """

    def setUp(self):
        super().setUp()
        self.traithandler.add(
            "test1",
            name="Test1",
            trait_type="trait",
            value="value",
            extra_val1="xvalue1",
            extra_val2="xvalue2",
        )
        self.trait = self.traithandler.get("test1")

    def test_init(self):
        self.assertEqual(
            self.trait._data,
            {
                "name": "Test1",
                "trait_type": "trait",
                "value": "value",
                "extra_val1": "xvalue1",
                "extra_val2": "xvalue2",
            },
        )

    def test_validate_input__valid(self):
        """Test valid validation input"""
        # all data supplied, and extras
        dat = {"name": "Test", "trait_type": "trait", "value": 10, "extra_val": 1000}
        expected = copy(dat)  # we must break link or return === dat always
        self.assertEqual(expected, traits.Trait.validate_input(traits.Trait, dat))

        # don't supply value, should get default
        dat = {
            "name": "Test",
            "trait_type": "trait",
            # missing value
            "extra_val": 1000,
        }
        expected = copy(dat)
        expected["value"] = traits.Trait.default_keys["value"]
        self.assertEqual(expected, traits.Trait.validate_input(traits.Trait, dat))

        # make sure extra values are cleaned if trait accepts no extras
        dat = {
            "name": "Test",
            "trait_type": "trait",
            "value": 10,
            "extra_val1": 1000,
            "extra_val2": "xvalue",
        }
        expected = copy(dat)
        expected.pop("extra_val1")
        expected.pop("extra_val2")
        with patch.object(traits.Trait, "allow_extra_properties", False):
            self.assertEqual(expected, traits.Trait.validate_input(traits.Trait, dat))

    def test_validate_input__fail(self):
        """Test failing validation"""
        dat = {
            # missing name
            "trait_type": "trait",
            "value": 10,
            "extra_val": 1000,
        }
        with self.assertRaises(traits.TraitException):
            traits.Trait.validate_input(traits.Trait, dat)

        # make value a required key
        mock_default_keys = {"value": traits.MandatoryTraitKey}
        with patch.object(traits.Trait, "default_keys", mock_default_keys):
            dat = {
                "name": "Trait",
                "trait_type": "trait",
                # missing value, now mandatory
                "extra_val": 1000,
            }
            with self.assertRaises(traits.TraitException):
                traits.Trait.validate_input(traits.Trait, dat)

    def test_trait_getset(self):
        """Get-set-del operations on trait"""
        self.assertEqual(self.trait.name, "Test1")
        self.assertEqual(self.trait["name"], "Test1")
        self.assertEqual(self.trait.value, "value")
        self.assertEqual(self.trait["value"], "value")
        self.assertEqual(self.trait.extra_val1, "xvalue1")
        self.assertEqual(self.trait["extra_val2"], "xvalue2")

        self.trait.value = 20
        self.assertEqual(self.trait["value"], 20)
        self.trait["value"] = 20
        self.assertEqual(self.trait.value, 20)
        self.trait.extra_val1 = 100
        self.assertEqual(self.trait.extra_val1, 100)
        # additional properties
        self.trait.foo = "bar"
        self.assertEqual(self.trait.foo, "bar")

        del self.trait.foo
        with self.assertRaises(KeyError):
            self.trait["foo"]
        with self.assertRaises(AttributeError):
            self.trait.foo
        del self.trait.extra_val1
        with self.assertRaises(AttributeError):
            self.trait.extra_val1
        del self.trait.value
        # fall back to default
        self.assertTrue(self.trait.value == traits.Trait.default_keys["value"])

    def test_repr(self):
        self.assertEqual(repr(self.trait), Something)
        self.assertEqual(str(self.trait), Something)


class TestTraitStatic(_TraitHandlerBase):
    """
    Test for static Traits
    """

    def setUp(self):
        super().setUp()
        self.traithandler.add(
            "test1",
            name="Test1",
            trait_type="static",
            base=1,
            mod=2,
            mult=1.0,
            extra_val1="xvalue1",
            extra_val2="xvalue2",
        )
        self.trait = self.traithandler.get("test1")

    def _get_values(self):
        return self.trait.base, self.trait.mod, self.trait.mult, self.trait.value

    def test_init(self):
        self.assertEqual(
            self._get_dbstore("test1"),
            {
                "name": "Test1",
                "trait_type": "static",
                "base": 1,
                "mod": 2,
                "mult": 1.0,
                "extra_val1": "xvalue1",
                "extra_val2": "xvalue2",
            },
        )

    def test_value(self):
        """value is (base + mod) * mult"""
        self.assertEqual(self._get_values(), (1, 2, 1.0, 3))
        self.trait.base += 4
        self.assertEqual(self._get_values(), (5, 2, 1.0, 7))
        self.trait.mod -= 1
        self.assertEqual(self._get_values(), (5, 1, 1.0, 6))
        self.trait.mult += 1.0
        self.assertEqual(self._get_values(), (5, 1, 2.0, 12))
        self.trait.mult = 0.75
        self.assertEqual(self._get_values(), (5, 1, 0.75, 4.5))

    def test_delete(self):
        """Deleting resets to default."""
        self.trait.mult = 2.0
        del self.trait.base
        self.assertEqual(self._get_values(), (0, 2, 2.0, 4))
        del self.trait.mult
        self.assertEqual(self._get_values(), (0, 2, 1.0, 2))
        del self.trait.mod
        self.assertEqual(self._get_values(), (0, 0, 1.0, 0))


class TestTraitCounter(_TraitHandlerBase):
    """
    Test for counter- Traits
    """

    def setUp(self):
        super().setUp()
        self.traithandler.add(
            "test1",
            name="Test1",
            trait_type="counter",
            base=1,
            mod=2,
            mult=1.0,
            min=0,
            max=10,
            extra_val1="xvalue1",
            extra_val2="xvalue2",
            descs={
                0: "range0",
                2: "range1",
                5: "range2",
                7: "range3",
            },
        )
        self.trait = self.traithandler.get("test1")

    def _get_values(self):
        """Get (base, mod, mult, value, min, max)."""
        return (
            self.trait.base,
            self.trait.mod,
            self.trait.mult,
            self.trait.value,
            self.trait.min,
            self.trait.max,
        )

    def test_init(self):
        self.assertEqual(
            self._get_dbstore("test1"),
            {
                "name": "Test1",
                "trait_type": "counter",
                "base": 1,
                "mod": 2,
                "mult": 1.0,
                "min": 0,
                "max": 10,
                "extra_val1": "xvalue1",
                "extra_val2": "xvalue2",
                "descs": {
                    0: "range0",
                    2: "range1",
                    5: "range2",
                    7: "range3",
                },
                "rate": 0,
                "ratetarget": None,
                "last_update": None,
            },
        )

    def test_value(self):
        """value is (current + mod) * mult, where current defaults to base"""
        self.assertEqual(self._get_values(), (1, 2, 1.0, 3, 0, 10))
        self.trait.base += 4
        self.assertEqual(self._get_values(), (5, 2, 1.0, 7, 0, 10))
        self.trait.mod -= 1
        self.assertEqual(self._get_values(), (5, 1, 1.0, 6, 0, 10))
        self.trait.mult += 1.0
        self.assertEqual(self._get_values(), (5, 1, 2.0, 10, 0, 10))

    def test_boundaries__minmax(self):
        """Test range"""
        # should not exceed min/max values
        self.trait.base += 20
        self.assertEqual(self._get_values(), (8, 2, 1.0, 10, 0, 10))
        self.trait.base = 100
        self.assertEqual(self._get_values(), (8, 2, 1.0, 10, 0, 10))
        self.trait.base -= 40
        self.assertEqual(self._get_values(), (-2, 2, 1.0, 0, 0, 10))
        self.trait.base = -100
        self.assertEqual(self._get_values(), (-2, 2, 1.0, 0, 0, 10))

    def test_boundaries__bigmod(self):
        """add a big mod"""
        self.trait.base = 5
        self.trait.mod = 100
        self.assertEqual(self._get_values(), (5, 5, 1.0, 10, 0, 10))
        self.trait.mod = -100
        self.assertEqual(self._get_values(), (5, -5, 1.0, 0, 0, 10))

    def test_boundaries__change_boundaries(self):
        """Change boundaries after base/mod change"""
        self.trait.base = 5
        self.trait.mod = -100
        self.trait.min = -20
        self.assertEqual(self._get_values(), (5, -5, 1.0, 0, -20, 10))
        self.trait.mod -= 100
        self.assertEqual(self._get_values(), (5, -25, 1.0, -20, -20, 10))
        self.trait.mod = 100
        self.trait.max = 20
        self.assertEqual(self._get_values(), (5, 5, 1.0, 10, -20, 20))
        self.trait.mod = 100
        self.assertEqual(self._get_values(), (5, 15, 1.0, 20, -20, 20))

    def test_boundaries__disable(self):
        """Disable and re-enable boundaries"""
        self.trait.base = 5
        self.trait.mod = 100
        self.assertEqual(self._get_values(), (5, 5, 1.0, 10, 0, 10))
        del self.trait.max
        self.assertEqual(self.trait.max, None)
        del self.trait.min
        self.assertEqual(self.trait.min, None)
        self.trait.base = 100
        self.assertEqual(self._get_values(), (100, 5, 1.0, 105, None, None))
        self.trait.base = -200
        self.assertEqual(self._get_values(), (-200, 5, 1.0, -195, None, None))

        # re-activate boundaries
        self.trait.max = 15
        self.trait.min = 10  # his is blocked since base+mod is lower
        self.assertEqual(self._get_values(), (-200, 5, 1.0, -195, -195, 15))

    def test_boundaries__inverse(self):
        """Set inverse boundaries - limited by base"""
        self.trait.mod = 0
        self.assertEqual(self._get_values(), (1, 0, 1.0, 1, 0, 10))
        self.trait.min = 20  # will be set to base
        self.assertEqual(self._get_values(), (1, 0, 1.0, 1, 1, 10))
        self.trait.max = -20
        self.assertEqual(self._get_values(), (1, 0, 1.0, 1, 1, 1))

    def test_current(self):
        """Modifying current value"""
        self.trait.current = 5
        self.assertEqual(self._get_values(), (1, 2, 1.0, 7, 0, 10))
        self.trait.current = 10
        self.assertEqual(self._get_values(), (1, 2, 1.0, 10, 0, 10))
        self.trait.current = 12
        self.assertEqual(self._get_values(), (1, 2, 1.0, 10, 0, 10))
        self.trait.current = -1
        self.assertEqual(self._get_values(), (1, 2, 1.0, 2, 0, 10))
        self.trait.current -= 10
        self.assertEqual(self._get_values(), (1, 2, 1.0, 2, 0, 10))

    def test_delete(self):
        """Deleting resets to default."""
        del self.trait.base
        self.assertEqual(self._get_values(), (0, 2, 1.0, 2, 0, 10))
        del self.trait.mod
        self.assertEqual(self._get_values(), (0, 0, 1.0, 0, 0, 10))
        del self.trait.min
        del self.trait.max
        self.assertEqual(self._get_values(), (0, 0, 1.0, 0, None, None))

    def test_percentage(self):
        """Test percentage calculation"""
        self.trait.base = 8
        self.trait.mod = 2
        self.trait.mult = 1.0
        self.trait.min = 0
        self.trait.max = 10
        self.assertEqual(self.trait.percent(), "100.0%")
        self.trait.current = 3
        self.assertEqual(self.trait.percent(), "50.0%")
        self.trait.current = 1
        self.assertEqual(self.trait.percent(), "30.0%")
        # have to lower this since max cannot be lowered below base+mod
        self.trait.mod = 1
        self.trait.current = 2
        self.trait.max -= 1
        self.assertEqual(self.trait.percent(), "33.3%")
        # open boundary
        del self.trait.min
        self.assertEqual(self.trait.percent(), "100.0%")

    def test_descs(self):
        """Test descriptions"""
        self.trait.min = -5
        self.trait.mod = 0
        self.assertEqual(self._get_values(), (1, 0, 1.0, 1, -5, 10))
        self.trait.current = -2
        self.assertEqual(self.trait.desc(), "range0")
        self.trait.current = 0
        self.assertEqual(self.trait.desc(), "range0")
        self.trait.current = 1
        self.assertEqual(self.trait.desc(), "range1")
        self.trait.current = 3
        self.assertEqual(self.trait.desc(), "range2")
        self.trait.current = 5
        self.assertEqual(self.trait.desc(), "range2")
        self.trait.current = 9
        self.assertEqual(self.trait.desc(), "range3")
        self.trait.current = 100
        self.assertEqual(self.trait.desc(), "range3")


class TestTraitCounterTimed(_TraitHandlerBase):
    """
    Test for trait with timer component
    """

    @patch("evennia.contrib.rpg.traits.traits.time", new=MagicMock(return_value=1000))
    def setUp(self):
        super().setUp()
        self.traithandler.add(
            "test1",
            name="Test1",
            trait_type="counter",
            base=1,
            mod=2,
            mult=1.0,
            min=0,
            max=100,
            extra_val1="xvalue1",
            extra_val2="xvalue2",
            descs={
                0: "range0",
                2: "range1",
                5: "range2",
                7: "range3",
            },
            rate=1,
            ratetarget=None,
        )
        self.trait = self.traithandler.get("test1")

    def _get_timer_data(self):
        return (
            self.trait.value,
            self.trait.current,
            self.trait.rate,
            self.trait._data["last_update"],
            self.trait.ratetarget,
        )

    @patch("evennia.contrib.rpg.traits.traits.time")
    def test_timer_rate(self, mock_time):
        """Test time stepping"""
        mock_time.return_value = 1000
        self.assertEqual(self._get_timer_data(), (3, 1, 1, 1000, None))
        mock_time.return_value = 1001
        self.assertEqual(self._get_timer_data(), (4, 2, 1, 1001, None))
        mock_time.return_value = 1096
        self.assertEqual(self._get_timer_data(), (99, 97, 1, 1096, None))
        # hit maximum boundary
        mock_time.return_value = 1120
        self.assertEqual(self._get_timer_data(), (100, 98, 1, None, None))
        mock_time.return_value = 1200
        self.assertEqual(self._get_timer_data(), (100, 98, 1, None, None))
        # drop current
        self.trait.current = 50
        self.assertEqual(self._get_timer_data(), (52, 50, 1, 1200, None))
        # set a new rate
        self.trait.rate = 2
        mock_time.return_value = 1210
        self.assertEqual(self._get_timer_data(), (72, 70, 2, 1210, None))
        self.trait.rate = -10
        mock_time.return_value = 1214
        self.assertEqual(self._get_timer_data(), (32, 30, -10, 1214, None))
        mock_time.return_value = 1218
        self.assertEqual(self._get_timer_data(), (0, -2, -10, None, None))

    @patch("evennia.contrib.rpg.traits.traits.time")
    def test_timer_ratetarget(self, mock_time):
        """test ratetarget"""
        mock_time.return_value = 1000
        self.trait.ratetarget = 60
        self.assertEqual(self._get_timer_data(), (3, 1, 1, 1000, 60))
        mock_time.return_value = 1056
        self.assertEqual(self._get_timer_data(), (59, 57, 1, 1056, 60))
        mock_time.return_value = 1057
        self.assertEqual(self._get_timer_data(), (60, 58, 1, None, 60))
        mock_time.return_value = 1060
        self.assertEqual(self._get_timer_data(), (60, 58, 1, None, 60))
        self.trait.ratetarget = 70
        mock_time.return_value = 1066
        self.assertEqual(self._get_timer_data(), (66, 64, 1, 1066, 70))
        mock_time.return_value = 1070
        self.assertEqual(self._get_timer_data(), (70, 68, 1, None, 70))


class TestTraitGauge(_TraitHandlerBase):
    def setUp(self):
        super().setUp()
        self.traithandler.add(
            "test1",
            name="Test1",
            trait_type="gauge",
            base=8,  # max = (base + mod) * mult
            mod=2,
            mult=1.0,
            extra_val1="xvalue1",
            extra_val2="xvalue2",
            descs={
                0: "range0",
                2: "range1",
                5: "range2",
                7: "range3",
            },
        )
        self.trait = self.traithandler.get("test1")

    def _get_values(self):
        """Get (base, mod, mult, value, min, max)."""
        return (
            self.trait.base,
            self.trait.mod,
            self.trait.mult,
            self.trait.value,
            self.trait.min,
            self.trait.max,
        )

    def test_init(self):
        self.assertEqual(
            self._get_dbstore("test1"),
            {
                "name": "Test1",
                "trait_type": "gauge",
                "base": 8,
                "mod": 2,
                "mult": 1.0,
                "min": 0,
                "extra_val1": "xvalue1",
                "extra_val2": "xvalue2",
                "descs": {
                    0: "range0",
                    2: "range1",
                    5: "range2",
                    7: "range3",
                },
                "rate": 0,
                "ratetarget": None,
                "last_update": None,
            },
        )

    def test_value(self):
        """value is current, where current defaults to base + mod"""
        # current unset - follows base + mod
        self.assertEqual(self._get_values(), (8, 2, 1.0, 10, 0, 10))
        self.trait.base += 4
        self.assertEqual(self._get_values(), (12, 2, 1.0, 14, 0, 14))
        self.trait.mod -= 1
        self.assertEqual(self._get_values(), (12, 1, 1.0, 13, 0, 13))
        self.trait.mult += 1.0
        self.assertEqual(self._get_values(), (12, 1, 2.0, 26, 0, 26))
        # set current, decouple from base + mod
        self.trait.current = 5
        self.assertEqual(self._get_values(), (12, 1, 2.0, 5, 0, 26))
        self.trait.mod += 1
        self.trait.base -= 4
        self.trait.mult -= 1.0
        self.assertEqual(self._get_values(), (8, 2, 1.0, 5, 0, 10))
        self.trait.min = -100
        self.trait.base = -20
        self.assertEqual(self._get_values(), (-20, 2, 1.0, -18, -100, -18))

    def test_boundaries__minmax(self):
        """Test range"""
        # current unset - tied to base + mod
        self.trait.base += 20
        self.assertEqual(self._get_values(), (28, 2, 1.0, 30, 0, 30))
        # set current - decouple from base + mod
        self.trait.current = 19
        self.assertEqual(self._get_values(), (28, 2, 1.0, 19, 0, 30))
        # test upper bound
        self.trait.current = 100
        self.assertEqual(self._get_values(), (28, 2, 1.0, 30, 0, 30))
        # with multiplier
        self.trait.mult = 2.0
        self.assertEqual(self._get_values(), (28, 2, 2.0, 30, 0, 60))
        self.trait.current = 100
        self.assertEqual(self._get_values(), (28, 2, 2.0, 60, 0, 60))
        # min defaults to 0
        self.trait.mult = 1.0
        self.trait.current = -10
        self.assertEqual(self._get_values(), (28, 2, 1.0, 0, 0, 30))
        self.trait.min = -20
        self.assertEqual(self._get_values(), (28, 2, 1.0, 0, -20, 30))
        self.trait.current = -10
        self.assertEqual(self._get_values(), (28, 2, 1.0, -10, -20, 30))

    def test_boundaries__bigmod(self):
        """add a big mod"""
        self.trait.base = 5
        self.trait.mod = 100
        self.assertEqual(self._get_values(), (5, 100, 1.0, 105, 0, 105))
        # restricted by min
        self.trait.mod = -100
        self.assertEqual(self._get_values(), (5, -5, 1.0, 0, 0, 0))
        self.trait.min = -200
        self.assertEqual(self._get_values(), (5, -5, 1.0, 0, -200, 0))

    def test_boundaries__change_boundaries(self):
        """Change boundaries after current change"""
        self.trait.current = 20
        self.assertEqual(self._get_values(), (8, 2, 1.0, 10, 0, 10))
        self.trait.mod = 102
        self.assertEqual(self._get_values(), (8, 102, 1.0, 10, 0, 110))
        # raising min past current value will force it upwards
        self.trait.min = 20
        self.assertEqual(self._get_values(), (8, 102, 1.0, 20, 20, 110))

    def test_boundaries__disable(self):
        """Disable and re-enable boundary"""
        self.trait.base = 5
        self.trait.min = 1
        self.assertEqual(self._get_values(), (5, 2, 1.0, 7, 1, 7))
        del self.trait.min
        self.assertEqual(self._get_values(), (5, 2, 1.0, 7, 0, 7))
        del self.trait.base
        del self.trait.mod
        self.assertEqual(self._get_values(), (0, 0, 1.0, 0, 0, 0))
        with self.assertRaises(traits.TraitException):
            del self.trait.max

    def test_boundaries__inverse(self):
        """Try to set reversed boundaries"""
        self.trait.mod = 0
        self.trait.base = -10  # limited by min
        self.assertEqual(self._get_values(), (0, 0, 1.0, 0, 0, 0))
        self.trait.min = -10
        self.assertEqual(self._get_values(), (0, 0, 1.0, 0, -10, 0))
        self.trait.base = -10
        self.assertEqual(self._get_values(), (-10, 0, 1.0, -10, -10, -10))
        self.min = 0  # limited by base + mod
        self.assertEqual(self._get_values(), (-10, 0, 1.0, -10, -10, -10))

    def test_current(self):
        """Modifying current value"""
        self.trait.base = 10
        self.trait.current = 5
        self.assertEqual(self._get_values(), (10, 2, 1.0, 5, 0, 12))
        self.trait.current = 10
        self.assertEqual(self._get_values(), (10, 2, 1.0, 10, 0, 12))
        self.trait.current = 12
        self.assertEqual(self._get_values(), (10, 2, 1.0, 12, 0, 12))
        self.trait.current = 0
        self.assertEqual(self._get_values(), (10, 2, 1.0, 0, 0, 12))
        self.trait.current = -1
        self.assertEqual(self._get_values(), (10, 2, 1.0, 0, 0, 12))

    def test_delete(self):
        """Deleting resets to default."""
        del self.trait.mod
        self.assertEqual(self._get_values(), (8, 0, 1.0, 8, 0, 8))
        self.trait.mod = 2
        del self.trait.base
        self.assertEqual(self._get_values(), (0, 2, 1.0, 2, 0, 2))
        del self.trait.min
        self.assertEqual(self._get_values(), (0, 2, 1.0, 2, 0, 2))
        self.trait.min = -10
        self.assertEqual(self._get_values(), (0, 2, 1.0, 2, -10, 2))
        del self.trait.min
        self.assertEqual(self._get_values(), (0, 2, 1.0, 2, 0, 2))

    def test_percentage(self):
        """Test percentage calculation"""
        self.assertEqual(self.trait.percent(), "100.0%")
        self.trait.current = 5
        self.assertEqual(self.trait.percent(), "50.0%")
        self.trait.current = 3
        self.assertEqual(self.trait.percent(), "30.0%")
        self.trait.mod -= 1
        self.assertEqual(self.trait.percent(), "33.3%")

    def test_descs(self):
        """Test descriptions"""
        self.trait.min = -5
        self.assertEqual(self._get_values(), (8, 2, 1.0, 10, -5, 10))
        self.trait.current = -2
        self.assertEqual(self.trait.desc(), "range0")
        self.trait.current = 0
        self.assertEqual(self.trait.desc(), "range0")
        self.trait.current = 1
        self.assertEqual(self.trait.desc(), "range1")
        self.trait.current = 3
        self.assertEqual(self.trait.desc(), "range2")
        self.trait.current = 5
        self.assertEqual(self.trait.desc(), "range2")
        self.trait.current = 9
        self.assertEqual(self.trait.desc(), "range3")
        self.trait.current = 100
        self.assertEqual(self.trait.desc(), "range3")


class TestTraitGaugeTimed(_TraitHandlerBase):
    """
    Test for trait with timer component
    """

    @patch("evennia.contrib.rpg.traits.traits.time", new=MagicMock(return_value=1000))
    def setUp(self):
        super().setUp()
        self.traithandler.add(
            "test1",
            name="Test1",
            trait_type="gauge",
            base=98,
            mod=2,
            min=0,
            extra_val1="xvalue1",
            extra_val2="xvalue2",
            descs={
                0: "range0",
                2: "range1",
                5: "range2",
                7: "range3",
            },
            rate=1,
            ratetarget=None,
        )
        self.trait = self.traithandler.get("test1")

    def _get_timer_data(self):
        return (
            self.trait.value,
            self.trait.current,
            self.trait.rate,
            self.trait._data["last_update"],
            self.trait.ratetarget,
        )

    @patch("evennia.contrib.rpg.traits.traits.time")
    def test_timer_rate(self, mock_time):
        """Test time stepping"""
        mock_time.return_value = 1000
        self.trait.current = 1
        self.assertEqual(self._get_timer_data(), (1, 1, 1, 1000, None))
        mock_time.return_value = 1001
        self.assertEqual(self._get_timer_data(), (2, 2, 1, 1001, None))
        mock_time.return_value = 1096
        self.assertEqual(self._get_timer_data(), (97, 97, 1, 1096, None))
        # hit maximum boundary
        mock_time.return_value = 1120
        self.assertEqual(self._get_timer_data(), (100, 100, 1, None, None))
        mock_time.return_value = 1200
        self.assertEqual(self._get_timer_data(), (100, 100, 1, None, None))
        # drop current
        self.trait.current = 50
        self.assertEqual(self._get_timer_data(), (50, 50, 1, 1200, None))
        # set a new rate
        self.trait.rate = 2
        mock_time.return_value = 1210
        self.assertEqual(self._get_timer_data(), (70, 70, 2, 1210, None))
        self.trait.rate = -10
        mock_time.return_value = 1214
        self.assertEqual(self._get_timer_data(), (30, 30, -10, 1214, None))
        mock_time.return_value = 1218
        self.assertEqual(self._get_timer_data(), (0, 0, -10, None, None))

    @patch("evennia.contrib.rpg.traits.traits.time")
    def test_timer_ratetarget(self, mock_time):
        """test ratetarget"""
        mock_time.return_value = 1000
        self.trait.current = 1
        self.trait.ratetarget = 60
        self.assertEqual(self._get_timer_data(), (1, 1, 1, 1000, 60))
        mock_time.return_value = 1056
        self.assertEqual(self._get_timer_data(), (57, 57, 1, 1056, 60))
        mock_time.return_value = 1059
        self.assertEqual(self._get_timer_data(), (60, 60, 1, None, 60))
        mock_time.return_value = 1060
        self.assertEqual(self._get_timer_data(), (60, 60, 1, None, 60))
        self.trait.ratetarget = 70
        mock_time.return_value = 1066
        self.assertEqual(self._get_timer_data(), (66, 66, 1, 1066, 70))
        mock_time.return_value = 1070
        self.assertEqual(self._get_timer_data(), (70, 70, 1, None, 70))


class TestNumericTraitOperators(BaseEvenniaTestCase):
    """Test case for numeric magic method implementations."""

    def setUp(self):
        # direct instantiation for testing only; use TraitHandler in production
        self.st = traits.Trait(
            {
                "name": "Strength",
                "trait_type": "trait",
                "value": 8,
            }
        )
        self.at = traits.Trait(
            {
                "name": "Attack",
                "trait_type": "trait",
                "value": 4,
            }
        )

    def tearDown(self):
        self.st, self.at = None, None

    def test_pos_shortcut(self):
        """overridden unary + operator returns `value` property"""
        self.assertIn(type(+self.st), (float, int))
        self.assertEqual(+self.st, self.st.value)
        self.assertEqual(+self.st, 8)

    def test_add_traits(self):
        """test addition of `Trait` objects"""
        # two Trait objects
        self.assertEqual(self.st + self.at, 12)
        # Trait and numeric
        self.assertEqual(self.st + 1, 9)
        self.assertEqual(1 + self.st, 9)

    def test_sub_traits(self):
        """test subtraction of `Trait` objects"""
        # two Trait objects
        self.assertEqual(self.st - self.at, 4)
        # Trait and numeric
        self.assertEqual(self.st - 1, 7)
        self.assertEqual(10 - self.st, 2)

    def test_mul_traits(self):
        """test multiplication of `Trait` objects"""
        # between two Traits
        self.assertEqual(self.st * self.at, 32)
        # between Trait and numeric
        self.assertEqual(self.at * 4, 16)
        self.assertEqual(4 * self.at, 16)

    def test_floordiv(self):
        """test floor division of `Trait` objects"""
        # between two Traits
        self.assertEqual(self.st // self.at, 2)
        # between Trait and numeric
        self.assertEqual(self.st // 2, 4)
        self.assertEqual(18 // self.st, 2)

    def test_comparisons_traits(self):
        """test equality comparison between `Trait` objects"""
        self.assertNotEqual(self.st, self.at)
        self.assertLess(self.at, self.st)
        self.assertLessEqual(self.at, self.st)
        self.assertGreater(self.st, self.at)
        self.assertGreaterEqual(self.st, self.at)

    def test_comparisons_numeric(self):
        """equality comparisons between `Trait` and numeric"""
        self.assertEqual(self.st, 8)
        self.assertEqual(8, self.st)
        self.assertNotEqual(self.st, 0)
        self.assertNotEqual(0, self.st)
        self.assertLess(self.st, 10)
        self.assertLess(0, self.st)
        self.assertLessEqual(self.st, 8)
        self.assertLessEqual(8, self.st)
        self.assertLessEqual(self.st, 10)
        self.assertLessEqual(0, self.st)
        self.assertGreater(self.st, 0)
        self.assertGreater(10, self.st)
        self.assertGreaterEqual(self.st, 8)
        self.assertGreaterEqual(8, self.st)
        self.assertGreaterEqual(self.st, 0)
        self.assertGreaterEqual(10, self.st)


class DummyCharacter(_MockObj):
    strength = traits.TraitProperty("Strength", trait_type="static", base=10, mod=2)
    hunting = traits.TraitProperty("Hunting skill", trait_type="counter", base=10, mod=1, max=100)
    health = traits.TraitProperty("Health value", trait_type="gauge", base=100)


class TestTraitFields(BaseEvenniaTestCase):
    """
    Test the TraitField class.

    """

    @patch("evennia.contrib.rpg.traits.traits._TRAIT_CLASS_PATHS", new=_TEST_TRAIT_CLASS_PATHS)
    def test_traitfields(self):
        obj = DummyCharacter()
        obj2 = DummyCharacter()

        self.assertEqual(12, obj.strength.value)
        self.assertEqual(11, obj.hunting.value)
        self.assertEqual(100, obj.health.value)

        obj.strength.base += 5
        self.assertEqual(17, obj.strength.value)

        obj.strength.berserk = True
        self.assertEqual(obj.strength.berserk, True)

        self.assertEqual(100, obj.traits.health)
        self.assertEqual(None, obj.traits.hp)

        # the traithandler still works
        obj.traits.health.current -= 1
        self.assertEqual(99, obj.health.value)

        # making sure Descriptors are separate
        self.assertEqual(12, obj2.strength.value)
        self.assertEqual(17, obj.strength.value)

        obj2.strength.base += 1
        obj.strength.base += 3

        self.assertEqual(13, obj2.strength.value)
        self.assertEqual(20, obj.strength.value)


class TraitContribTestingChar(DefaultCharacter):
    HP = traits.TraitProperty("health", trait_type="trait", value=5)


class TraitPropertyTestCase(EvenniaTest):
    """
    Test atomic updating.

    """

    character_typeclass = TraitContribTestingChar

    def test_round1(self):
        self.char1.HP.value = 1

    def test_round2(self):
        self.char1.HP.value = 2
