from __future__ import absolute_import, unicode_literals

import os

import mock

import pkg_resources

import pytest

from mopidy import config, exceptions, ext

from tests import IsA, any_unicode


class DummyExtension(ext.Extension):
    dist_name = 'Mopidy-Foobar'
    ext_name = 'foobar'
    version = '1.2.3'

    def get_default_config(self):
        return '[foobar]\nenabled = true'


any_testextension = IsA(DummyExtension)


class TestExtension(object):

    @pytest.fixture
    def extension(self):
        return ext.Extension()

    def test_dist_name_is_none(self, extension):
        assert extension.dist_name is None

    def test_ext_name_is_none(self, extension):
        assert extension.ext_name is None

    def test_version_is_none(self, extension):
        assert extension.version is None

    def test_get_default_config_raises_not_implemented(self, extension):
        with pytest.raises(NotImplementedError):
            extension.get_default_config()

    def test_get_config_schema_returns_extension_schema(self, extension):
        schema = extension.get_config_schema()
        assert isinstance(schema['enabled'], config.Boolean)

    def test_validate_environment_does_nothing_by_default(self, extension):
        assert extension.validate_environment() is None

    def test_setup_raises_not_implemented(self, extension):
        with pytest.raises(NotImplementedError):
            extension.setup(None)

    def test_get_cache_dir_raises_assertion_error(self, extension):
        config = {'core': {'cache_dir': '/tmp'}}
        with pytest.raises(AssertionError):  # ext_name not set
            ext.Extension.get_cache_dir(config)

    def test_get_config_dir_raises_assertion_error(self, extension):
        config = {'core': {'config_dir': '/tmp'}}
        with pytest.raises(AssertionError):  # ext_name not set
            ext.Extension.get_config_dir(config)

    def test_get_data_dir_raises_assertion_error(self, extension):
        config = {'core': {'data_dir': '/tmp'}}
        with pytest.raises(AssertionError):  # ext_name not set
            ext.Extension.get_data_dir(config)


class TestLoadExtensions(object):

    @pytest.yield_fixture
    def iter_entry_points_mock(self, request):
        patcher = mock.patch('pkg_resources.iter_entry_points')
        iter_entry_points = patcher.start()
        iter_entry_points.return_value = []
        yield iter_entry_points
        patcher.stop()

    def test_no_extensions(self, iter_entry_points_mock):
        iter_entry_points_mock.return_value = []
        assert ext.load_extensions() == []

    def test_load_extensions(self, iter_entry_points_mock):
        mock_entry_point = mock.Mock()
        mock_entry_point.resolve.return_value = DummyExtension

        iter_entry_points_mock.return_value = [mock_entry_point]

        expected = ext.ExtensionData(
            any_testextension, mock_entry_point, IsA(config.ConfigSchema),
            any_unicode, None)

        assert ext.load_extensions() == [expected]

    def test_gets_wrong_class(self, iter_entry_points_mock):

        class WrongClass(object):
            pass

        mock_entry_point = mock.Mock()
        mock_entry_point.resolve.return_value = WrongClass

        iter_entry_points_mock.return_value = [mock_entry_point]

        assert ext.load_extensions() == []

    def test_gets_instance(self, iter_entry_points_mock):
        mock_entry_point = mock.Mock()
        mock_entry_point.resolve.return_value = DummyExtension()

        iter_entry_points_mock.return_value = [mock_entry_point]

        assert ext.load_extensions() == []

    def test_creating_instance_fails(self, iter_entry_points_mock):
        mock_extension = mock.Mock(spec=ext.Extension)
        mock_extension.side_effect = Exception

        mock_entry_point = mock.Mock()
        mock_entry_point.resolve.return_value = mock_extension

        iter_entry_points_mock.return_value = [mock_entry_point]

        assert ext.load_extensions() == []

    def test_get_config_schema_fails(self, iter_entry_points_mock):
        mock_entry_point = mock.Mock()
        mock_entry_point.resolve.return_value = DummyExtension

        iter_entry_points_mock.return_value = [mock_entry_point]

        with mock.patch.object(DummyExtension, 'get_config_schema') as get:
            get.side_effect = Exception

            assert ext.load_extensions() == []
            get.assert_called_once_with()

    def test_get_default_config_fails(self, iter_entry_points_mock):
        mock_entry_point = mock.Mock()
        mock_entry_point.resolve.return_value = DummyExtension

        iter_entry_points_mock.return_value = [mock_entry_point]

        with mock.patch.object(DummyExtension, 'get_default_config') as get:
            get.side_effect = Exception

            assert ext.load_extensions() == []
            get.assert_called_once_with()

    def test_get_command_fails(self, iter_entry_points_mock):
        mock_entry_point = mock.Mock()
        mock_entry_point.resolve.return_value = DummyExtension

        iter_entry_points_mock.return_value = [mock_entry_point]

        with mock.patch.object(DummyExtension, 'get_command') as get:
            get.side_effect = Exception

            assert ext.load_extensions() == []
            get.assert_called_once_with()


class TestValidateExtensionData(object):

    @pytest.fixture
    def ext_data(self):
        extension = DummyExtension()

        entry_point = mock.Mock()
        entry_point.name = extension.ext_name

        schema = extension.get_config_schema()
        defaults = extension.get_default_config()
        command = extension.get_command()

        return ext.ExtensionData(
            extension, entry_point, schema, defaults, command)

    def test_name_mismatch(self, ext_data):
        ext_data.entry_point.name = 'barfoo'
        assert not ext.validate_extension_data(ext_data)

    def test_distribution_not_found(self, ext_data):
        error = pkg_resources.DistributionNotFound
        ext_data.entry_point.require.side_effect = error
        assert not ext.validate_extension_data(ext_data)

    def test_version_conflict(self, ext_data):
        error = pkg_resources.VersionConflict
        ext_data.entry_point.require.side_effect = error
        assert not ext.validate_extension_data(ext_data)

    def test_entry_point_require_exception(self, ext_data):
        ext_data.entry_point.require.side_effect = Exception

        # Hope that entry points are well behaved, so exception will bubble.
        with pytest.raises(Exception):
            assert not ext.validate_extension_data(ext_data)

    def test_extenions_validate_environment_error(self, ext_data):
        extension = ext_data.extension
        with mock.patch.object(extension, 'validate_environment') as validate:
            validate.side_effect = exceptions.ExtensionError('error')

            assert not ext.validate_extension_data(ext_data)
            validate.assert_called_once_with()

    def test_extenions_validate_environment_exception(self, ext_data):
        extension = ext_data.extension
        with mock.patch.object(extension, 'validate_environment') as validate:
            validate.side_effect = Exception

            assert not ext.validate_extension_data(ext_data)
            validate.assert_called_once_with()

    def test_missing_schema(self, ext_data):
        ext_data = ext_data._replace(config_schema=None)
        assert not ext.validate_extension_data(ext_data)

    def test_schema_that_is_missing_enabled(self, ext_data):
        del ext_data.config_schema['enabled']
        ext_data.config_schema['baz'] = config.String()
        assert not ext.validate_extension_data(ext_data)

    def test_schema_with_wrong_types(self, ext_data):
        ext_data.config_schema['enabled'] = 123
        assert not ext.validate_extension_data(ext_data)

    def test_schema_with_invalid_type(self, ext_data):
        ext_data.config_schema['baz'] = 123
        assert not ext.validate_extension_data(ext_data)

    def test_no_default_config(self, ext_data):
        ext_data = ext_data._replace(config_defaults=None)
        assert not ext.validate_extension_data(ext_data)

    def test_get_cache_dir(self, ext_data):
        core_cache_dir = '/tmp'
        config = {'core': {'cache_dir': core_cache_dir}}
        extension = ext_data.extension

        with mock.patch.object(ext.path, 'get_or_create_dir'):
            cache_dir = extension.get_cache_dir(config)

        expected = os.path.join(core_cache_dir, extension.ext_name)
        assert cache_dir == expected

    def test_get_config_dir(self, ext_data):
        core_config_dir = '/tmp'
        config = {'core': {'config_dir': core_config_dir}}
        extension = ext_data.extension

        with mock.patch.object(ext.path, 'get_or_create_dir'):
            config_dir = extension.get_config_dir(config)

        expected = os.path.join(core_config_dir, extension.ext_name)
        assert config_dir == expected

    def test_get_data_dir(self, ext_data):
        core_data_dir = '/tmp'
        config = {'core': {'data_dir': core_data_dir}}
        extension = ext_data.extension

        with mock.patch.object(ext.path, 'get_or_create_dir'):
            data_dir = extension.get_data_dir(config)

        expected = os.path.join(core_data_dir, extension.ext_name)
        assert data_dir == expected
