import os
import sys
import unittest
from unittest.mock import patch

import frodocs
from frodocs.config import config_options
from frodocs.config.base import Config


class OptionallyRequiredTest(unittest.TestCase):

    def test_empty(self):

        option = config_options.OptionallyRequired()
        value = option.validate(None)
        self.assertEqual(value, None)

        self.assertEqual(option.is_required(), False)

    def test_required(self):

        option = config_options.OptionallyRequired(required=True)
        self.assertRaises(config_options.ValidationError,
                          option.validate, None)

        self.assertEqual(option.is_required(), True)

    def test_required_no_default(self):

        option = config_options.OptionallyRequired(required=True)
        value = option.validate(2)
        self.assertEqual(2, value)

    def test_default(self):

        option = config_options.OptionallyRequired(default=1)
        value = option.validate(None)
        self.assertEqual(1, value)

    def test_replace_default(self):

        option = config_options.OptionallyRequired(default=1)
        value = option.validate(2)
        self.assertEqual(2, value)


class TypeTest(unittest.TestCase):

    def test_single_type(self):

        option = config_options.Type(str)
        value = option.validate("Testing")
        self.assertEqual(value, "Testing")

    def test_multiple_types(self):
        option = config_options.Type((list, tuple))

        value = option.validate([1, 2, 3])
        self.assertEqual(value, [1, 2, 3])

        value = option.validate((1, 2, 3))
        self.assertEqual(value, (1, 2, 3))

        self.assertRaises(config_options.ValidationError,
                          option.validate, {'a': 1})

    def test_length(self):
        option = config_options.Type(str, length=7)

        value = option.validate("Testing")
        self.assertEqual(value, "Testing")

        self.assertRaises(config_options.ValidationError,
                          option.validate, "Testing Long")


class ChoiceTest(unittest.TestCase):

    def test_valid_choice(self):
        option = config_options.Choice(('python', 'node'))
        value = option.validate('python')
        self.assertEqual(value, 'python')

    def test_invalid_choice(self):
        option = config_options.Choice(('python', 'node'))
        self.assertRaises(
            config_options.ValidationError, option.validate, 'go')

    def test_invalid_choices(self):
        self.assertRaises(ValueError, config_options.Choice, '')
        self.assertRaises(ValueError, config_options.Choice, [])
        self.assertRaises(ValueError, config_options.Choice, 5)


class IpAddressTest(unittest.TestCase):

    def test_valid_address(self):
        addr = '127.0.0.1:8000'

        option = config_options.IpAddress()
        value = option.validate(addr)
        self.assertEqual(str(value), addr)
        self.assertEqual(value.host, '127.0.0.1')
        self.assertEqual(value.port, 8000)

    def test_valid_IPv6_address(self):
        addr = '[::1]:8000'

        option = config_options.IpAddress()
        value = option.validate(addr)
        self.assertEqual(str(value), addr)
        self.assertEqual(value.host, '[::1]')
        self.assertEqual(value.port, 8000)

    def test_named_address(self):
        addr = 'localhost:8000'

        option = config_options.IpAddress()
        value = option.validate(addr)
        self.assertEqual(str(value), addr)
        self.assertEqual(value.host, 'localhost')
        self.assertEqual(value.port, 8000)

    def test_default_address(self):
        addr = '127.0.0.1:8000'

        option = config_options.IpAddress(default=addr)
        value = option.validate(None)
        self.assertEqual(str(value), addr)
        self.assertEqual(value.host, '127.0.0.1')
        self.assertEqual(value.port, 8000)

    def test_invalid_address_format(self):
        option = config_options.IpAddress()
        self.assertRaises(
            config_options.ValidationError,
            option.validate, '127.0.0.18000'
        )

    def test_invalid_address_type(self):
        option = config_options.IpAddress()
        self.assertRaises(
            config_options.ValidationError,
            option.validate, 123
        )

    def test_invalid_address_port(self):
        option = config_options.IpAddress()
        self.assertRaises(
            config_options.ValidationError,
            option.validate, '127.0.0.1:foo'
        )

    def test_invalid_address_missing_port(self):
        option = config_options.IpAddress()
        self.assertRaises(
            config_options.ValidationError,
            option.validate, '127.0.0.1'
        )


class URLTest(unittest.TestCase):

    def test_valid_url(self):

        url = "https://frodo.ml"

        option = config_options.URL()
        value = option.validate(url)
        self.assertEqual(value, url)

    def test_invalid_url(self):

        option = config_options.URL()
        self.assertRaises(config_options.ValidationError,
                          option.validate, "www.frodo.ml")

    def test_invalid(self):

        option = config_options.URL()
        self.assertRaises(config_options.ValidationError,
                          option.validate, 1)


class RepoURLTest(unittest.TestCase):

    def test_repo_name_github(self):

        option = config_options.RepoURL()
        config = {'repo_url': "https://github.com/frodocs/frodocs"}
        option.post_validation(config, 'repo_url')
        self.assertEqual(config['repo_name'], "GitHub")

    def test_repo_name_bitbucket(self):

        option = config_options.RepoURL()
        config = {'repo_url': "https://bitbucket.org/gutworth/six/"}
        option.post_validation(config, 'repo_url')
        self.assertEqual(config['repo_name'], "Bitbucket")

    def test_repo_name_gitlab(self):

        option = config_options.RepoURL()
        config = {'repo_url': "https://gitlab.com/gitlab-org/gitlab-ce/"}
        option.post_validation(config, 'repo_url')
        self.assertEqual(config['repo_name'], "GitLab")

    def test_repo_name_custom(self):

        option = config_options.RepoURL()
        config = {'repo_url': "https://launchpad.net/python-tuskarclient"}
        option.post_validation(config, 'repo_url')
        self.assertEqual(config['repo_name'], "Launchpad")

    def test_edit_uri_github(self):

        option = config_options.RepoURL()
        config = {'repo_url': "https://github.com/frodocs/frodocs"}
        option.post_validation(config, 'repo_url')
        self.assertEqual(config['edit_uri'], 'edit/master/docs/')

    def test_edit_uri_bitbucket(self):

        option = config_options.RepoURL()
        config = {'repo_url': "https://bitbucket.org/gutworth/six/"}
        option.post_validation(config, 'repo_url')
        self.assertEqual(config['edit_uri'], 'src/default/docs/')

    def test_edit_uri_gitlab(self):

        option = config_options.RepoURL()
        config = {'repo_url': "https://gitlab.com/gitlab-org/gitlab-ce/"}
        option.post_validation(config, 'repo_url')
        self.assertEqual(config['edit_uri'], 'edit/master/docs/')

    def test_edit_uri_custom(self):

        option = config_options.RepoURL()
        config = {'repo_url': "https://launchpad.net/python-tuskarclient"}
        option.post_validation(config, 'repo_url')
        self.assertEqual(config.get('edit_uri'), '')

    def test_repo_name_custom_and_empty_edit_uri(self):

        option = config_options.RepoURL()
        config = {'repo_url': "https://github.com/frodocs/frodocs",
                  'repo_name': 'frodocs'}
        option.post_validation(config, 'repo_url')
        self.assertEqual(config.get('edit_uri'), 'edit/master/docs/')


class DirTest(unittest.TestCase):

    def test_valid_dir(self):

        d = os.path.dirname(__file__)
        option = config_options.Dir(exists=True)
        value = option.validate(d)
        self.assertEqual(d, value)

    def test_missing_dir(self):

        d = os.path.join("not", "a", "real", "path", "I", "hope")
        option = config_options.Dir()
        value = option.validate(d)
        self.assertEqual(os.path.abspath(d), value)

    def test_missing_dir_but_required(self):

        d = os.path.join("not", "a", "real", "path", "I", "hope")
        option = config_options.Dir(exists=True)
        self.assertRaises(config_options.ValidationError,
                          option.validate, d)

    def test_file(self):
        d = __file__
        option = config_options.Dir(exists=True)
        self.assertRaises(config_options.ValidationError,
                          option.validate, d)

    def test_incorrect_type_attribute_error(self):
        option = config_options.Dir()
        self.assertRaises(config_options.ValidationError,
                          option.validate, 1)

    def test_incorrect_type_type_error(self):
        option = config_options.Dir()
        self.assertRaises(config_options.ValidationError,
                          option.validate, [])

    def test_dir_unicode(self):
        cfg = Config(
            [('dir', config_options.Dir())],
            config_file_path=os.path.join(os.path.abspath('.'), 'frodocs.yml'),
        )

        test_config = {
            'dir': 'юникод'
        }

        cfg.load_dict(test_config)

        fails, warns = cfg.validate()

        self.assertEqual(len(fails), 0)
        self.assertEqual(len(warns), 0)
        self.assertIsInstance(cfg['dir'], str)

    def test_dir_filesystemencoding(self):
        cfg = Config(
            [('dir', config_options.Dir())],
            config_file_path=os.path.join(os.path.abspath('.'), 'frodocs.yml'),
        )

        test_config = {
            'dir': 'Übersicht'.encode(encoding=sys.getfilesystemencoding())
        }

        cfg.load_dict(test_config)

        fails, warns = cfg.validate()

        # str does not include byte strings so validation fails
        self.assertEqual(len(fails), 1)
        self.assertEqual(len(warns), 0)

    def test_dir_bad_encoding_fails(self):
        cfg = Config(
            [('dir', config_options.Dir())],
            config_file_path=os.path.join(os.path.abspath('.'), 'frodocs.yml'),
        )

        test_config = {
            'dir': 'юникод'.encode(encoding='ISO 8859-5')
        }

        cfg.load_dict(test_config)

        fails, warns = cfg.validate()

        self.assertEqual(len(fails), 1)
        self.assertEqual(len(warns), 0)

    def test_config_dir_prepended(self):
        base_path = os.path.abspath('.')
        cfg = Config(
            [('dir', config_options.Dir())],
            config_file_path=os.path.join(base_path, 'frodocs.yml'),
        )

        test_config = {
            'dir': 'foo'
        }

        cfg.load_dict(test_config)

        fails, warns = cfg.validate()

        self.assertEqual(len(fails), 0)
        self.assertEqual(len(warns), 0)
        self.assertIsInstance(cfg['dir'], str)
        self.assertEqual(cfg['dir'], os.path.join(base_path, 'foo'))

    def test_dir_is_config_dir_fails(self):
        cfg = Config(
            [('dir', config_options.Dir())],
            config_file_path=os.path.join(os.path.abspath('.'), 'frodocs.yml'),
        )

        test_config = {
            'dir': '.'
        }

        cfg.load_dict(test_config)

        fails, warns = cfg.validate()

        self.assertEqual(len(fails), 1)
        self.assertEqual(len(warns), 0)


class SiteDirTest(unittest.TestCase):

    def validate_config(self, config):
        """ Given a config with values for site_dir and doc_dir, run site_dir post_validation. """
        site_dir = config_options.SiteDir()
        docs_dir = config_options.Dir()

        fname = os.path.join(os.path.abspath('..'), 'frodocs.yml')

        config['docs_dir'] = docs_dir.validate(config['docs_dir'])
        config['site_dir'] = site_dir.validate(config['site_dir'])

        schema = [
            ('site_dir', site_dir),
            ('docs_dir', docs_dir),
        ]
        cfg = Config(schema, fname)
        cfg.load_dict(config)
        failed, warned = cfg.validate()

        if failed:
            raise config_options.ValidationError(failed)

        return True

    def test_doc_dir_in_site_dir(self):

        j = os.path.join
        # The parent dir is not the same on every system, so use the actual dir name
        parent_dir = frodocs.__file__.split(os.sep)[-3]

        test_configs = (
            {'docs_dir': j('site', 'docs'), 'site_dir': 'site'},
            {'docs_dir': 'docs', 'site_dir': '.'},
            {'docs_dir': '.', 'site_dir': '.'},
            {'docs_dir': 'docs', 'site_dir': ''},
            {'docs_dir': '', 'site_dir': ''},
            {'docs_dir': j('..', parent_dir, 'docs'), 'site_dir': 'docs'},
            {'docs_dir': 'docs', 'site_dir': '/'}
        )

        for test_config in test_configs:
            self.assertRaises(config_options.ValidationError,
                              self.validate_config, test_config)

    def test_site_dir_in_docs_dir(self):

        j = os.path.join

        test_configs = (
            {'docs_dir': 'docs', 'site_dir': j('docs', 'site')},
            {'docs_dir': '.', 'site_dir': 'site'},
            {'docs_dir': '', 'site_dir': 'site'},
            {'docs_dir': '/', 'site_dir': 'site'},
        )

        for test_config in test_configs:
            self.assertRaises(config_options.ValidationError,
                              self.validate_config, test_config)

    def test_common_prefix(self):
        """ Legitimate settings with common prefixes should not fail validation. """

        test_configs = (
            {'docs_dir': 'docs', 'site_dir': 'docs-site'},
            {'docs_dir': 'site-docs', 'site_dir': 'site'},
        )

        for test_config in test_configs:
            assert self.validate_config(test_config)


class ThemeTest(unittest.TestCase):

    def test_theme_as_string(self):

        option = config_options.Theme()
        value = option.validate("frodocs")
        self.assertEqual({'name': 'frodocs'}, value)

    def test_uninstalled_theme_as_string(self):

        option = config_options.Theme()
        self.assertRaises(config_options.ValidationError,
                          option.validate, "frodocs2")

    def test_theme_default(self):
        option = config_options.Theme(default='frodocs')
        value = option.validate(None)
        self.assertEqual({'name': 'frodocs'}, value)

    def test_theme_as_simple_config(self):

        config = {
            'name': 'frodocs'
        }
        option = config_options.Theme()
        value = option.validate(config)
        self.assertEqual(config, value)

    def test_theme_as_complex_config(self):

        config = {
            'name': 'frodocs',
            'custom_dir': 'custom',
            'static_templates': ['sitemap.html'],
            'show_sidebar': False
        }
        option = config_options.Theme()
        value = option.validate(config)
        self.assertEqual(config, value)

    def test_theme_name_is_none(self):

        config = {
            'name': None
        }
        option = config_options.Theme()
        value = option.validate(config)
        self.assertEqual(config, value)

    def test_theme_config_missing_name(self):

        config = {
            'custom_dir': 'custom',
        }
        option = config_options.Theme()
        self.assertRaises(config_options.ValidationError,
                          option.validate, config)

    def test_uninstalled_theme_as_config(self):

        config = {
            'name': 'frodocs2'
        }
        option = config_options.Theme()
        self.assertRaises(config_options.ValidationError,
                          option.validate, config)

    def test_theme_invalid_type(self):

        config = ['frodocs2']
        option = config_options.Theme()
        self.assertRaises(config_options.ValidationError,
                          option.validate, config)


class NavTest(unittest.TestCase):

    def test_old_format(self):

        option = config_options.Nav()
        self.assertRaises(
            config_options.ValidationError,
            option.validate,
            [['index.md', ], ]
        )

    def test_provided_dict(self):

        option = config_options.Nav()
        value = option.validate([
            'index.md',
            {"Page": "page.md"}
        ])
        self.assertEqual(['index.md', {'Page': 'page.md'}], value)

        option.post_validation({'extra_stuff': []}, 'extra_stuff')

    def test_provided_empty(self):

        option = config_options.Nav()
        value = option.validate([])
        self.assertEqual(None, value)

        option.post_validation({'extra_stuff': []}, 'extra_stuff')

    def test_invalid_type(self):

        option = config_options.Nav()
        self.assertRaises(config_options.ValidationError,
                          option.validate, {})

    def test_invalid_config(self):

        option = config_options.Nav()
        self.assertRaises(config_options.ValidationError,
                          option.validate, [[], 1])


class PrivateTest(unittest.TestCase):

    def test_defined(self):

        option = config_options.Private()
        self.assertRaises(config_options.ValidationError,
                          option.validate, 'somevalue')


class MarkdownExtensionsTest(unittest.TestCase):

    @patch('markdown.Markdown')
    def test_simple_list(self, mockMd):
        option = config_options.MarkdownExtensions()
        config = {
            'markdown_extensions': ['foo', 'bar']
        }
        config['markdown_extensions'] = option.validate(config['markdown_extensions'])
        option.post_validation(config, 'markdown_extensions')
        self.assertEqual({
            'markdown_extensions': ['foo', 'bar'],
            'mdx_configs': {}
        }, config)

    @patch('markdown.Markdown')
    def test_list_dicts(self, mockMd):
        option = config_options.MarkdownExtensions()
        config = {
            'markdown_extensions': [
                {'foo': {'foo_option': 'foo value'}},
                {'bar': {'bar_option': 'bar value'}},
                {'baz': None}
            ]
        }
        config['markdown_extensions'] = option.validate(config['markdown_extensions'])
        option.post_validation(config, 'markdown_extensions')
        self.assertEqual({
            'markdown_extensions': ['foo', 'bar', 'baz'],
            'mdx_configs': {
                'foo': {'foo_option': 'foo value'},
                'bar': {'bar_option': 'bar value'}
            }
        }, config)

    @patch('markdown.Markdown')
    def test_mixed_list(self, mockMd):
        option = config_options.MarkdownExtensions()
        config = {
            'markdown_extensions': [
                'foo',
                {'bar': {'bar_option': 'bar value'}}
            ]
        }
        config['markdown_extensions'] = option.validate(config['markdown_extensions'])
        option.post_validation(config, 'markdown_extensions')
        self.assertEqual({
            'markdown_extensions': ['foo', 'bar'],
            'mdx_configs': {
                'bar': {'bar_option': 'bar value'}
            }
        }, config)

    @patch('markdown.Markdown')
    def test_builtins(self, mockMd):
        option = config_options.MarkdownExtensions(builtins=['meta', 'toc'])
        config = {
            'markdown_extensions': ['foo', 'bar']
        }
        config['markdown_extensions'] = option.validate(config['markdown_extensions'])
        option.post_validation(config, 'markdown_extensions')
        self.assertEqual({
            'markdown_extensions': ['meta', 'toc', 'foo', 'bar'],
            'mdx_configs': {}
        }, config)

    def test_duplicates(self):
        option = config_options.MarkdownExtensions(builtins=['meta', 'toc'])
        config = {
            'markdown_extensions': ['meta', 'toc']
        }
        config['markdown_extensions'] = option.validate(config['markdown_extensions'])
        option.post_validation(config, 'markdown_extensions')
        self.assertEqual({
            'markdown_extensions': ['meta', 'toc'],
            'mdx_configs': {}
        }, config)

    def test_builtins_config(self):
        option = config_options.MarkdownExtensions(builtins=['meta', 'toc'])
        config = {
            'markdown_extensions': [
                {'toc': {'permalink': True}}
            ]
        }
        config['markdown_extensions'] = option.validate(config['markdown_extensions'])
        option.post_validation(config, 'markdown_extensions')
        self.assertEqual({
            'markdown_extensions': ['meta', 'toc'],
            'mdx_configs': {'toc': {'permalink': True}}
        }, config)

    @patch('markdown.Markdown')
    def test_configkey(self, mockMd):
        option = config_options.MarkdownExtensions(configkey='bar')
        config = {
            'markdown_extensions': [
                {'foo': {'foo_option': 'foo value'}}
            ]
        }
        config['markdown_extensions'] = option.validate(config['markdown_extensions'])
        option.post_validation(config, 'markdown_extensions')
        self.assertEqual({
            'markdown_extensions': ['foo'],
            'bar': {
                'foo': {'foo_option': 'foo value'}
            }
        }, config)

    def test_none(self):
        option = config_options.MarkdownExtensions(default=[])
        config = {
            'markdown_extensions': None
        }
        config['markdown_extensions'] = option.validate(config['markdown_extensions'])
        option.post_validation(config, 'markdown_extensions')
        self.assertEqual({
            'markdown_extensions': [],
            'mdx_configs': {}
        }, config)

    @patch('markdown.Markdown')
    def test_not_list(self, mockMd):
        option = config_options.MarkdownExtensions()
        self.assertRaises(config_options.ValidationError,
                          option.validate, 'not a list')

    @patch('markdown.Markdown')
    def test_invalid_config_option(self, mockMd):
        option = config_options.MarkdownExtensions()
        config = {
            'markdown_extensions': [
                {'foo': 'not a dict'}
            ]
        }
        self.assertRaises(
            config_options.ValidationError,
            option.validate, config['markdown_extensions']
        )

    @patch('markdown.Markdown')
    def test_invalid_config_item(self, mockMd):
        option = config_options.MarkdownExtensions()
        config = {
            'markdown_extensions': [
                ['not a dict']
            ]
        }
        self.assertRaises(
            config_options.ValidationError,
            option.validate, config['markdown_extensions']
        )

    @patch('markdown.Markdown')
    def test_invalid_dict_item(self, mockMd):
        option = config_options.MarkdownExtensions()
        config = {
            'markdown_extensions': [
                {'key1': 'value', 'key2': 'too many keys'}
            ]
        }
        self.assertRaises(
            config_options.ValidationError,
            option.validate, config['markdown_extensions']
        )

    def test_unknown_extension(self):
        option = config_options.MarkdownExtensions()
        config = {
            'markdown_extensions': ['unknown']
        }
        self.assertRaises(
            config_options.ValidationError,
            option.validate, config['markdown_extensions']
        )
