__author__ = "Brian O'Neill"  # BTO
__version__ = '0.1.14'
__doc__ = """
    100% coverage of deco_settings.py
"""

from unittest import TestCase
from log_calls import DecoSetting, DecoSettingsMapping
from log_calls.log_calls import DecoSettingEnabled, DecoSettingHistory

from collections import OrderedDict
import inspect

# - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# helper
# - - - - - - - - - - - - - - - - - - - - - - - - - - - -
import re


def collapse_whitespace(s):
    s = re.sub(r'\n', ' ', s)
    s = re.sub(r'\s\s+', ' ', s)
    return s.strip()


##############################################################################
# DecoSetting tests
##############################################################################
class TestDecoSetting(TestCase):
    info_plain = None
    info_extended = None

    @classmethod
    def setUpClass(cls):
        cls.info_plain = DecoSetting('set_once', int, 15,
                                     allow_falsy=True, mutable=False)

        cls.hidden = DecoSetting('hidden', bool, True,
                                 allow_falsy=True, visible=False)

        # with extra fields
        cls.info_extended = DecoSetting('extended', tuple, ('Joe', "Schmoe"),
                                        allow_falsy=True, allow_indirect=False,
                                        extra1='Tom', extra2='Dick', extra3='Harry')

    def test___init__1(self):
        """without any additional attributes"""
        self.assertEqual(self.info_plain.name, 'set_once')
        self.assertEqual(self.info_plain.final_type, int)
        self.assertEqual(self.info_plain.default, 15)
        self.assertEqual(self.info_plain.allow_falsy, True)
        self.assertEqual(self.info_plain.allow_indirect, True)
        self.assertEqual(self.info_plain.mutable, False)
        self.assertEqual(self.info_plain.visible, True)
        self.assertEqual(self.info_plain._user_attrs, [])

    def test___init__2(self):
        """WITH additional attributes."""
        self.assertEqual(self.hidden.name, 'hidden')
        self.assertEqual(self.hidden.final_type, bool)
        self.assertEqual(self.hidden.default, True)
        self.assertEqual(self.hidden.allow_falsy, True)
        self.assertEqual(self.hidden.allow_indirect, False)  # because visible=False
        self.assertEqual(self.hidden.mutable, True)
        self.assertEqual(self.hidden.visible, False)

    def test___init__3(self):
        """WITH additional attributes."""
        self.assertEqual(self.info_extended.name, 'extended')
        self.assertEqual(self.info_extended.final_type, tuple)
        self.assertEqual(self.info_extended.default, ('Joe', "Schmoe"))
        self.assertEqual(self.info_extended.allow_falsy, True)
        self.assertEqual(self.info_extended.allow_indirect, False)
        self.assertEqual(self.info_extended.mutable, True)
        self.assertEqual(self.info_extended.visible, True)
        self.assertEqual(self.info_extended._user_attrs, ['extra1', 'extra2', 'extra3'])
        self.assertEqual(self.info_extended.extra1, 'Tom')
        self.assertEqual(self.info_extended.extra2, 'Dick')
        self.assertEqual(self.info_extended.extra3, 'Harry')

    def test___repr__1(self):
        plain_repr = "DecoSetting('set_once', int, 15, allow_falsy=True, " \
                     "allow_indirect=True, mutable=False, visible=True)"
        self.assertEqual(repr(self.info_plain), plain_repr)

    def test___repr__2(self):
        hidden_repr = "DecoSetting('hidden', bool, True, allow_falsy=True, " \
                      "allow_indirect=False, mutable=True, visible=False)"
        self.assertEqual(repr(self.hidden), hidden_repr)

    def test___repr__3(self):
        ext_repr = "DecoSetting('extended', tuple, ('Joe', 'Schmoe'), " \
                   "allow_falsy=True, allow_indirect=False, " \
                   "mutable=True, visible=True, " \
                   "extra1='Tom', extra2='Dick', extra3='Harry')"
        self.assertEqual(repr(self.info_extended), ext_repr)


##############################################################################
# DecoSetting tests
##############################################################################

class TestDecoSettingsMapping(TestCase):

    # placeholders:
    _settings_mapping = OrderedDict()

    @classmethod
    def setUpClass(cls):
        cls._settings = (
            DecoSettingEnabled('enabled'),
            DecoSetting('folderol',         str,            '',            allow_falsy=True,  allow_indirect=False),
            DecoSetting('my_setting',       str,            'on',          allow_falsy=False, allow_indirect=True),
            DecoSetting('your_setting',     str,            'off',         allow_falsy=False, allow_indirect=False,
                        mutable=False),
            DecoSettingHistory('history', visible=False),
        )
        DecoSettingsMapping.register_class_settings('TestDecoSettingsMapping',
                                                    cls._settings)

    def setUp(self):
        """__init__(self, *, deco_class, **values_dict)"""
        self._settings_mapping = DecoSettingsMapping(
            deco_class=self.__class__,
            # the rest are what DecoSettingsMapping calls **values_dict
            enabled=True,
            folderol='bar',
            my_setting='eek',       # str but doesn't end in '=' --> not indirect
            your_setting='Howdy',
            history=False
        )

    def test_register_class_settings(self):
        self.assertIn('TestDecoSettingsMapping', DecoSettingsMapping._classname2SettingsData_dict)
        od = DecoSettingsMapping._classname2SettingsData_dict['TestDecoSettingsMapping']
        self.assertIsInstance(od, OrderedDict)  # implies od, od is not None, etc.
#        self.assertEqual(len(od), len(self._settings))
        names = tuple(map(lambda s: s.name, self._settings))
        self.assertEqual(tuple(od), names)

    def test___init__(self):
        """setUp does what __init__ ordinarily does:
            __init__(self, *, deco_class, **values_dict)"""
        self.assertEqual(self._settings_mapping.deco_class, self.__class__)

    ## TO DO - howdya test THIS?
    ## Implicitly, it gets tested and the descriptors it creates get tested.
    ## make_setting_descriptor is a classmethod.
    # def test_make_setting_descriptor(self):
    #     descr = DecoSettingsMapping.make_setting_descriptor('key')
    #     self.fail()

    def test__deco_class_settings_dict(self):
        """property. Can't use/call/test _deco_class_settings_dict till __init__"""
        od = self._settings_mapping._deco_class_settings_dict
        self.assertIs(od, self._settings_mapping._classname2SettingsData_dict[
            self._settings_mapping.deco_class.__name__]
        )
        self.assertEqual(list(self._settings_mapping),
                         ['enabled', 'folderol', 'my_setting', 'your_setting']
        )

    def test_registered_class_settings_repr(self):
        settings_repr = """
            DecoSettingsMapping.register_class_settings(
                TestDecoSettingsMapping,
                [DecoSetting('enabled', int, False, allow_falsy=True, allow_indirect=True, mutable=True, visible=True),
                 DecoSetting('folderol', str, '', allow_falsy=True, allow_indirect=False, mutable=True, visible=True),
                 DecoSetting('my_setting', str, 'on', allow_falsy=False, allow_indirect=True, mutable=True, visible=True),
                 DecoSetting('your_setting', str, 'off', allow_falsy=False, allow_indirect=False, mutable=False, visible=True),
                 DecoSetting('history', bool, False, allow_falsy=True, allow_indirect=False, mutable=True, visible=False)
            ])
        """
        self.assertEqual(
            collapse_whitespace(self._settings_mapping.registered_class_settings_repr()),
            collapse_whitespace(settings_repr)
        )

    def test__handlers(self):
        self.assertEqual(self._settings_mapping._handlers,
                         (('enabled',), ('history',)))

    def test__pre_call_handlers(self):
        self.assertEqual(self._settings_mapping._pre_call_handlers,
                         ('enabled',))

    def test__post_call_handlers(self):
        self.assertEqual(self._settings_mapping._post_call_handlers,
                         ('history',))

    def test__get_DecoSetting(self):
        for key in self._settings_mapping._deco_class_settings_dict:
            self.assertEqual(self._settings_mapping._get_DecoSetting(key),
                             self._settings_mapping._deco_class_settings_dict[key])
            self.assertEqual(key, self._settings_mapping._get_DecoSetting(key).name)

    def test___getitem__(self):
        """Test descriptors too"""
        mapping = self._settings_mapping
        self.assertEqual(mapping['enabled'], True)
        self.assertEqual(mapping['folderol'], 'bar')
        self.assertEqual(mapping['my_setting'], 'eek')
        self.assertEqual(mapping['your_setting'], "Howdy")

        self.assertEqual(mapping.enabled, True)
        self.assertEqual(mapping.folderol, 'bar')
        self.assertEqual(mapping.my_setting, 'eek')
        self.assertEqual(mapping.your_setting, "Howdy")

        def get_bad_item(bad_key):
            return mapping[bad_key]

        def get_bad_attr(bad_attr):
            return getattr(mapping, bad_attr)

        def get_hidden_item():
            return mapping['history']

        def get_hidden_attr():
            return mapping.history

        self.assertRaises(KeyError, get_bad_item, 'no_such_key')
        self.assertRaises(AttributeError, get_bad_attr, 'no_such_attr')

        self.assertRaises(KeyError, get_hidden_item)
        self.assertRaises(AttributeError, get_hidden_attr)

    def test___setitem__(self):
        """Test descriptors too.
        Test your_setting -- mutable=False"""
        mapping = self._settings_mapping

        mapping['enabled'] = False
        mapping['folderol'] = 'BAR'
        mapping['my_setting'] = 'OUCH'

        def set_item_not_mutable(s):
            mapping['your_setting'] = s

        self.assertRaises(ValueError, set_item_not_mutable, "HARK! Who goes there?")

        self.assertEqual(mapping['enabled'], False)
        self.assertEqual(mapping['folderol'], 'BAR')
        self.assertEqual(mapping['my_setting'], 'OUCH')
        self.assertEqual(mapping['your_setting'], "Howdy")      # not mutable, so not changed!

        # Now set back to mostly 'orig' values using descriptors
        mapping.enabled = True
        mapping.folderol = 'bar'
        mapping.my_setting = 'eek'

        def set_attr_not_mutable(s):
            mapping.your_setting = s

        self.assertRaises(ValueError, set_attr_not_mutable, "This won't work either.")

        self.assertEqual(mapping.enabled, True)
        self.assertEqual(mapping.folderol, 'bar')
        self.assertEqual(mapping.my_setting, 'eek')
        self.assertEqual(mapping.your_setting, "Howdy")

        mapping.__setitem__('your_setting', 'not howdy', _force_mutable=True)
        self.assertEqual(mapping.your_setting, "not howdy")

        # Now test setting a nonexistent key, & a nonexistent attr/descr
        def set_bad_item(bad_key, val):
            mapping[bad_key] = val

        self.assertRaises(KeyError, set_bad_item, 'no_such_key', 413)
        ## BUT The following does NOT raise an AttributeError
        # def set_bad_attr(bad_attr, val):
        #     setattr(mapping, bad_attr, val)
        #
        # self.assertRaises(AttributeError, set_bad_attr, 'no_such_attr', 495)
        mapping.no_such_attr = 495
        self.assertEqual(mapping.no_such_attr, 495)

        # Test setting settings with visible=False
        def set_hidden_item():
            mapping['history'] = False

        def set_hidden_attr():
            mapping.history = False

        self.assertRaises(KeyError, set_hidden_item)

        # Get value of history setting
        history_val = mapping.get_final_value('history', fparams=None)
        # You CAN add an attribute called 'history'
        #  BUT it is *not* the 'history' setting:
        mapping.history = not history_val
        self.assertEqual(mapping.history, not history_val)
        # The setting is unchanged:
        self.assertEqual(history_val, mapping.get_final_value('history', fparams=None))

        # Actually change the value:
        mapping.__setitem__('history', not history_val, _force_visible=True)
        # get new val of history
        new_val = mapping.get_final_value('history', fparams=None)
        self.assertEqual(new_val, not history_val)

    def test___len__(self):
        self.assertEqual(len(self._settings_mapping), 4)

    def test___iter__(self):
        names = [name for name in self._settings_mapping]
        self.assertEqual(names, ['enabled', 'folderol', 'my_setting', 'your_setting'])

    def test_items(self):
        items = [item for item in self._settings_mapping.items()]
        self.assertEqual(items,
                         [('enabled', self._settings_mapping['enabled']),
                          ('folderol', self._settings_mapping['folderol']),
                          ('my_setting', self._settings_mapping['my_setting']),
                          ('your_setting', self._settings_mapping['your_setting'])]
        )

    def test___contains__(self):
        self.assertIn('enabled', self._settings_mapping)
        self.assertIn('folderol', self._settings_mapping)
        self.assertIn('my_setting', self._settings_mapping)
        self.assertIn('your_setting', self._settings_mapping)

        self.assertNotIn('history', self._settings_mapping)
        self.assertNotIn('no_such_key', self._settings_mapping)

    def test___repr__(self):
        the_repr = """
            DecoSettingsMapping(
                deco_class=TestDecoSettingsMapping,
                ** {       'enabled': True,
                    'folderol': 'bar',
                    'my_setting': 'eek',
                    'your_setting': 'Howdy'} )
        """
        self.assertEqual(
            collapse_whitespace(repr(self._settings_mapping)),
            collapse_whitespace(the_repr),
        )

    def test___str__(self):
        ## print("self._settings_mapping str: %s" % str(self._settings_mapping))
        ##     {'folderol': 'bar', 'my_setting': 'eek', 'your_setting': 'Howdy', 'enabled': True}
        self.assertDictEqual(
            eval(str(self._settings_mapping)),
            {'folderol': 'bar', 'my_setting': 'eek', 'your_setting': 'Howdy', 'enabled': True}
        )

    def test_update(self):
        mapping = self._settings_mapping

        d = {'enabled': False, 'folderol': 'tomfoolery', 'my_setting': 'balderdash=', 'your_setting': "Goodbye."}
        mapping.update(**d)                                 # pass as keywords
        self.assertEqual(mapping.enabled, False)
        self.assertEqual(mapping.folderol, 'tomfoolery')
        self.assertEqual(mapping.my_setting, 'balderdash=')
        self.assertEqual(mapping.your_setting, 'Howdy')     # NOT changed, and no exception
        self.assertEqual(len(mapping), 4)

        mapping.enabled = not mapping.enabled
        mapping.folderol = 'nada'
        mapping.my_setting = "something-new"
        mapping.update(d)                                   # pass as dict
        self.assertEqual(mapping.enabled, False)
        self.assertEqual(mapping.folderol, 'tomfoolery')
        self.assertEqual(mapping.my_setting, 'balderdash=')
        self.assertEqual(mapping.your_setting, 'Howdy')     # NOT changed, and no exception
        self.assertEqual(len(mapping), 4)

        d1 = {'enabled': False, 'folderol': 'gibberish'}
        d2 = {'enabled': True, 'my_setting': 'hokum='}

        mapping.update(d1, d2)
        self.assertEqual(mapping.enabled, True)
        self.assertEqual(mapping.folderol, 'gibberish')
        self.assertEqual(mapping.my_setting, 'hokum=')

        mapping.update(d1, d2, **d)
        self.assertEqual(mapping.enabled, False)
        self.assertEqual(mapping.folderol, 'tomfoolery')
        self.assertEqual(mapping.my_setting, 'balderdash=')

        self.assertRaises(
            KeyError,
            mapping.update,
            no_such_setting=True
        )
        self.assertRaises(
            KeyError,
            mapping.update,
            history=True
        )

    def test_as_OrderedDict(self):
        self.assertDictEqual(
            OrderedDict([('enabled', True), ('folderol', 'bar'), ('my_setting', 'eek'), ('your_setting', 'Howdy')]),
            self._settings_mapping.as_OrderedDict()
        )

    def test_as_dict(self):
        self.assertDictEqual(self._settings_mapping.as_dict(),
                             {'folderol': 'bar', 'my_setting': 'eek', 'your_setting': 'Howdy', 'enabled': True})

    def test__get_tagged_value(self):
        mapping = self._settings_mapping
        mapping['enabled'] = "enabled_kwd"
        mapping['folderol'] = 'my_setting_kwd='
        mapping['my_setting'] = 'OUCH'

        self.assertEqual(mapping._get_tagged_value('enabled'), (True, 'enabled_kwd'))
        self.assertEqual(mapping._get_tagged_value('folderol'), (False, 'my_setting_kwd='))
        self.assertEqual(mapping._get_tagged_value('my_setting'), (False, 'OUCH'))
        self.assertEqual(mapping._get_tagged_value('your_setting'), (False, 'Howdy'))

        def bad_key():
            mapping._get_tagged_value('no_such_key')

        self.assertRaises(KeyError, bad_key)

    def test_get_final_value(self):
        mapping = self._settings_mapping
        v = mapping.get_final_value('enabled', fparams=None)
        self.assertEqual(v, True)

        mapping['enabled'] = 'enabled_kwd='
        d = {'enabled_kwd': 17}
        v = mapping.get_final_value('enabled', d, fparams=None)
        self.assertEqual(v, 17)

        def f(a, enabled_kwd=3):
            pass

        fparams = inspect.signature(f).parameters
        v = mapping.get_final_value('enabled', fparams=fparams)
        self.assertEqual(v, 3)

        def g(a, wrong_kwd='nevermind'):
            pass
        gparams = inspect.signature(g).parameters
        v = mapping.get_final_value('enabled', fparams=gparams)
        self.assertEqual(v, False)

        def h(a, enabled_kwd=[]):
            pass
        hparams = inspect.signature(h).parameters
        v = mapping.get_final_value('enabled', fparams=hparams)
        self.assertEqual(v, False)
