#!/usr/bin/env python


import unittest
from unittest import mock
import os

from frodocs import plugins
from frodocs import config


class DummyPlugin(plugins.BasePlugin):
    config_scheme = (
        ('foo', config.config_options.Type(str, default='default foo')),
        ('bar', config.config_options.Type(int, default=0)),
        ('dir', config.config_options.Dir(exists=False)),
    )

    def on_pre_page(self, content, **kwargs):
        """ modify page content by prepending `foo` config value. """
        return '{} {}'.format(self.config['foo'], content)

    def on_nav(self, item, **kwargs):
        """ do nothing (return None) to not modify item. """
        return None

    def on_page_read_source(self, **kwargs):
        """ create new source by prepending `foo` config value to 'source'. """
        return '{} {}'.format(self.config['foo'], 'source')

    def on_pre_build(self, **kwargs):
        """ do nothing (return None). """
        return None


class TestPluginClass(unittest.TestCase):

    def test_valid_plugin_options(self):
        test_dir = 'test'

        options = {
            'foo': 'some value',
            'dir': test_dir,
        }

        cfg_fname = os.path.join('tmp', 'test', 'fname.yml')
        cfg_fname = os.path.abspath(cfg_fname)

        cfg_dirname = os.path.dirname(cfg_fname)
        expected = os.path.join(cfg_dirname, test_dir)

        expected = {
            'foo': 'some value',
            'bar': 0,
            'dir': expected,
        }

        plugin = DummyPlugin()
        errors, warnings = plugin.load_config(options, config_file_path=cfg_fname)
        self.assertEqual(plugin.config, expected)
        self.assertEqual(errors, [])
        self.assertEqual(warnings, [])

    def test_invalid_plugin_options(self):

        plugin = DummyPlugin()
        errors, warnings = plugin.load_config({'foo': 42})
        self.assertEqual(len(errors), 1)
        self.assertIn('foo', errors[0])
        self.assertEqual(warnings, [])

        errors, warnings = plugin.load_config({'bar': 'a string'})
        self.assertEqual(len(errors), 1)
        self.assertIn('bar', errors[0])
        self.assertEqual(warnings, [])

        errors, warnings = plugin.load_config({'invalid_key': 'value'})
        self.assertEqual(errors, [])
        self.assertEqual(len(warnings), 1)
        self.assertIn('invalid_key', warnings[0])


class TestPluginCollection(unittest.TestCase):

    def test_set_plugin_on_collection(self):
        collection = plugins.PluginCollection()
        plugin = DummyPlugin()
        collection['foo'] = plugin
        self.assertEqual([(k, v) for k, v in collection.items()], [('foo', plugin)])

    def test_set_multiple_plugins_on_collection(self):
        collection = plugins.PluginCollection()
        plugin1 = DummyPlugin()
        collection['foo'] = plugin1
        plugin2 = DummyPlugin()
        collection['bar'] = plugin2
        self.assertEqual([(k, v) for k, v in collection.items()], [('foo', plugin1), ('bar', plugin2)])

    def test_run_event_on_collection(self):
        collection = plugins.PluginCollection()
        plugin = DummyPlugin()
        plugin.load_config({'foo': 'new'})
        collection['foo'] = plugin
        self.assertEqual(collection.run_event('pre_page', 'page content'), 'new page content')

    def test_run_event_twice_on_collection(self):
        collection = plugins.PluginCollection()
        plugin1 = DummyPlugin()
        plugin1.load_config({'foo': 'new'})
        collection['foo'] = plugin1
        plugin2 = DummyPlugin()
        plugin2.load_config({'foo': 'second'})
        collection['bar'] = plugin2
        self.assertEqual(collection.run_event('pre_page', 'page content'),
                         'second new page content')

    def test_event_returns_None(self):
        collection = plugins.PluginCollection()
        plugin = DummyPlugin()
        plugin.load_config({'foo': 'new'})
        collection['foo'] = plugin
        self.assertEqual(collection.run_event('nav', 'nav item'), 'nav item')

    def test_event_empty_item(self):
        collection = plugins.PluginCollection()
        plugin = DummyPlugin()
        plugin.load_config({'foo': 'new'})
        collection['foo'] = plugin
        self.assertEqual(collection.run_event('page_read_source'), 'new source')

    def test_event_empty_item_returns_None(self):
        collection = plugins.PluginCollection()
        plugin = DummyPlugin()
        plugin.load_config({'foo': 'new'})
        collection['foo'] = plugin
        self.assertEqual(collection.run_event('pre_build'), None)

    def test_run_undefined_event_on_collection(self):
        collection = plugins.PluginCollection()
        self.assertEqual(collection.run_event('pre_page', 'page content'), 'page content')

    def test_run_unknown_event_on_collection(self):
        collection = plugins.PluginCollection()
        self.assertRaises(KeyError, collection.run_event, 'unknown', 'page content')


MockEntryPoint = mock.Mock()
MockEntryPoint.configure_mock(**{'name': 'sample', 'load.return_value': DummyPlugin})


@mock.patch('pkg_resources.iter_entry_points', return_value=[MockEntryPoint])
class TestPluginConfig(unittest.TestCase):

    def test_plugin_config_without_options(self, mock_class):

        cfg = {'plugins': ['sample']}
        option = config.config_options.Plugins()
        cfg['plugins'] = option.validate(cfg['plugins'])

        self.assertIsInstance(cfg['plugins'], plugins.PluginCollection)
        self.assertIn('sample', cfg['plugins'])
        self.assertIsInstance(cfg['plugins']['sample'], plugins.BasePlugin)
        expected = {
            'foo': 'default foo',
            'bar': 0,
            'dir': None,
        }
        self.assertEqual(cfg['plugins']['sample'].config, expected)

    def test_plugin_config_with_options(self, mock_class):

        cfg = {
            'plugins': [{
                'sample': {
                    'foo': 'foo value',
                    'bar': 42
                }
            }]
        }
        option = config.config_options.Plugins()
        cfg['plugins'] = option.validate(cfg['plugins'])

        self.assertIsInstance(cfg['plugins'], plugins.PluginCollection)
        self.assertIn('sample', cfg['plugins'])
        self.assertIsInstance(cfg['plugins']['sample'], plugins.BasePlugin)
        expected = {
            'foo': 'foo value',
            'bar': 42,
            'dir': None,
        }
        self.assertEqual(cfg['plugins']['sample'].config, expected)

    def test_plugin_config_empty_list_with_empty_default(self, mock_class):
        cfg = {'plugins': []}
        option = config.config_options.Plugins(default=[])
        cfg['plugins'] = option.validate(cfg['plugins'])

        self.assertIsInstance(cfg['plugins'], plugins.PluginCollection)
        self.assertEqual(len(cfg['plugins']), 0)

    def test_plugin_config_empty_list_with_default(self, mock_class):
        # Default is ignored
        cfg = {'plugins': []}
        option = config.config_options.Plugins(default=['sample'])
        cfg['plugins'] = option.validate(cfg['plugins'])

        self.assertIsInstance(cfg['plugins'], plugins.PluginCollection)
        self.assertEqual(len(cfg['plugins']), 0)

    def test_plugin_config_none_with_empty_default(self, mock_class):
        cfg = {'plugins': None}
        option = config.config_options.Plugins(default=[])
        cfg['plugins'] = option.validate(cfg['plugins'])

        self.assertIsInstance(cfg['plugins'], plugins.PluginCollection)
        self.assertEqual(len(cfg['plugins']), 0)

    def test_plugin_config_none_with_default(self, mock_class):
        # Default is used.
        cfg = {'plugins': None}
        option = config.config_options.Plugins(default=['sample'])
        cfg['plugins'] = option.validate(cfg['plugins'])

        self.assertIsInstance(cfg['plugins'], plugins.PluginCollection)
        self.assertIn('sample', cfg['plugins'])
        self.assertIsInstance(cfg['plugins']['sample'], plugins.BasePlugin)
        expected = {
            'foo': 'default foo',
            'bar': 0,
            'dir': None,
        }
        self.assertEqual(cfg['plugins']['sample'].config, expected)

    def test_plugin_config_uninstalled(self, mock_class):

        cfg = {'plugins': ['uninstalled']}
        option = config.config_options.Plugins()
        self.assertRaises(config.base.ValidationError, option.validate, cfg['plugins'])

    def test_plugin_config_not_list(self, mock_class):

        cfg = {'plugins': 'sample'}  # should be a list
        option = config.config_options.Plugins()
        self.assertRaises(config.base.ValidationError, option.validate, cfg['plugins'])

    def test_plugin_config_multivalue_dict(self, mock_class):

        cfg = {
            'plugins': [{
                'sample': {
                    'foo': 'foo value',
                    'bar': 42
                },
                'extra_key': 'baz'
            }]
        }
        option = config.config_options.Plugins()
        self.assertRaises(config.base.ValidationError, option.validate, cfg['plugins'])

    def test_plugin_config_not_string_or_dict(self, mock_class):

        cfg = {
            'plugins': [('not a string or dict',)]
        }
        option = config.config_options.Plugins()
        self.assertRaises(config.base.ValidationError, option.validate, cfg['plugins'])

    def test_plugin_config_options_not_dict(self, mock_class):

        cfg = {
            'plugins': [{
                'sample': 'not a dict'
            }]
        }
        option = config.config_options.Plugins()
        self.assertRaises(config.base.ValidationError, option.validate, cfg['plugins'])
