import os
import tempfile
import unittest
from tempfile import TemporaryDirectory

from mkdocs import exceptions
from mkdocs.config import base, defaults
from mkdocs.config.config_options import BaseConfigOption


class ConfigBaseTests(unittest.TestCase):

    def test_unrecognised_keys(self):

        c = base.Config(schema=defaults.DEFAULT_SCHEMA)
        c.load_dict({
            'not_a_valid_config_option': "test"
        })

        failed, warnings = c.validate()

        self.assertEqual(warnings, [
            ('not_a_valid_config_option',
                'Unrecognised configuration name: not_a_valid_config_option')
        ])

    def test_missing_required(self):

        c = base.Config(schema=defaults.DEFAULT_SCHEMA)

        errors, warnings = c.validate()

        self.assertEqual(len(errors), 1)
        self.assertEqual(errors[0][0], 'site_name')
        self.assertEqual(str(errors[0][1]), 'Required configuration not provided.')

        self.assertEqual(len(warnings), 0)

    def test_load_from_file(self):
        """
        Users can explicitly set the config file using the '--config' option.
        Allows users to specify a config other than the default `mkdocs.yml`.
        """

        temp_dir = TemporaryDirectory()
        config_file = open(os.path.join(temp_dir.name, 'mkdocs.yml'), 'w')
        os.mkdir(os.path.join(temp_dir.name, 'docs'))
        try:
            config_file.write("site_name: MkDocs Test\n")
            config_file.flush()
            config_file.close()

            cfg = base.load_config(config_file=config_file.name)
            self.assertTrue(isinstance(cfg, base.Config))
            self.assertEqual(cfg['site_name'], 'MkDocs Test')
        finally:
            os.remove(config_file.name)
            temp_dir.cleanup()

    def test_load_from_missing_file(self):

        self.assertRaises(exceptions.ConfigurationError,
                          base.load_config, config_file='missing_file.yml')

    def test_load_from_open_file(self):
        """
        `load_config` can accept an open file descriptor.
        """

        temp_dir = TemporaryDirectory()
        temp_path = temp_dir.name
        config_fname = os.path.join(temp_path, 'mkdocs.yml')

        config_file = open(config_fname, 'w+')
        os.mkdir(os.path.join(temp_path, 'docs'))
        try:
            config_file.write("site_name: MkDocs Test\n")
            config_file.flush()

            cfg = base.load_config(config_file=config_file)
            self.assertTrue(isinstance(cfg, base.Config))
            self.assertEqual(cfg['site_name'], 'MkDocs Test')
            # load_config will always close the file
            self.assertTrue(config_file.closed)
        finally:
            temp_dir.cleanup()

    def test_load_from_closed_file(self):
        """
        The `serve` command with auto-reload may pass in a closed file descriptor.
        Ensure `load_config` reloads the closed file.
        """

        temp_dir = TemporaryDirectory()
        config_file = open(os.path.join(temp_dir.name, 'mkdocs.yml'), 'w')
        os.mkdir(os.path.join(temp_dir.name, 'docs'))

        try:
            config_file.write("site_name: MkDocs Test\n")
            config_file.flush()
            config_file.close()

            cfg = base.load_config(config_file=config_file)
            self.assertTrue(isinstance(cfg, base.Config))
            self.assertEqual(cfg['site_name'], 'MkDocs Test')
        finally:
            temp_dir.cleanup()

    def test_load_from_deleted_file(self):
        """
        Deleting the config file could trigger a server reload.
        """

        config_file = tempfile.NamedTemporaryFile('w', delete=False)
        try:
            config_file.write("site_name: MkDocs Test\n")
            config_file.flush()
            config_file.close()
        finally:
            os.remove(config_file.name)
        self.assertRaises(exceptions.ConfigurationError,
                          base.load_config, config_file=config_file)

    def test_load_missing_required(self):
        """
        `site_name` is a required setting.
        """

        config_file = tempfile.NamedTemporaryFile('w', delete=False)
        try:
            config_file.write(
                "site_dir: output\nsite_uri: https://www.mkdocs.org\n")
            config_file.flush()
            config_file.close()

            self.assertRaises(exceptions.ConfigurationError,
                              base.load_config, config_file=config_file.name)
        finally:
            os.remove(config_file.name)

    def test_pre_validation_error(self):
        class InvalidConfigOption(BaseConfigOption):
            def pre_validation(self, config, key_name):
                raise base.ValidationError('pre_validation error')

        c = base.Config(schema=(('invalid_option', InvalidConfigOption()), ))

        errors, warnings = c.validate()

        self.assertEqual(len(errors), 1)
        self.assertEqual(errors[0][0], 'invalid_option')
        self.assertEqual(str(errors[0][1]), 'pre_validation error')
        self.assertTrue(isinstance(errors[0][1], base.ValidationError))
        self.assertEqual(len(warnings), 0)

    def test_run_validation_error(self):
        class InvalidConfigOption(BaseConfigOption):
            def run_validation(self, value):
                raise base.ValidationError('run_validation error')

        c = base.Config(schema=(('invalid_option', InvalidConfigOption()), ))

        errors, warnings = c.validate()

        self.assertEqual(len(errors), 1)
        self.assertEqual(errors[0][0], 'invalid_option')
        self.assertEqual(str(errors[0][1]), 'run_validation error')
        self.assertTrue(isinstance(errors[0][1], base.ValidationError))
        self.assertEqual(len(warnings), 0)

    def test_post_validation_error(self):
        class InvalidConfigOption(BaseConfigOption):
            def post_validation(self, config, key_name):
                raise base.ValidationError('post_validation error')

        c = base.Config(schema=(('invalid_option', InvalidConfigOption()), ))

        errors, warnings = c.validate()

        self.assertEqual(len(errors), 1)
        self.assertEqual(errors[0][0], 'invalid_option')
        self.assertEqual(str(errors[0][1]), 'post_validation error')
        self.assertTrue(isinstance(errors[0][1], base.ValidationError))
        self.assertEqual(len(warnings), 0)

    def test_pre_and_run_validation_errors(self):
        """ A pre_validation error does not stop run_validation from running. """
        class InvalidConfigOption(BaseConfigOption):
            def pre_validation(self, config, key_name):
                raise base.ValidationError('pre_validation error')

            def run_validation(self, value):
                raise base.ValidationError('run_validation error')

        c = base.Config(schema=(('invalid_option', InvalidConfigOption()), ))

        errors, warnings = c.validate()

        self.assertEqual(len(errors), 2)
        self.assertEqual(errors[0][0], 'invalid_option')
        self.assertEqual(str(errors[0][1]), 'pre_validation error')
        self.assertTrue(isinstance(errors[0][1], base.ValidationError))
        self.assertEqual(errors[1][0], 'invalid_option')
        self.assertEqual(str(errors[1][1]), 'run_validation error')
        self.assertTrue(isinstance(errors[1][1], base.ValidationError))
        self.assertEqual(len(warnings), 0)

    def test_run_and_post_validation_errors(self):
        """ A run_validation error stops post_validation from running. """
        class InvalidConfigOption(BaseConfigOption):
            def run_validation(self, value):
                raise base.ValidationError('run_validation error')

            def post_validation(self, config, key_name):
                raise base.ValidationError('post_validation error')

        c = base.Config(schema=(('invalid_option', InvalidConfigOption()), ))

        errors, warnings = c.validate()

        self.assertEqual(len(errors), 1)
        self.assertEqual(errors[0][0], 'invalid_option')
        self.assertEqual(str(errors[0][1]), 'run_validation error')
        self.assertTrue(isinstance(errors[0][1], base.ValidationError))
        self.assertEqual(len(warnings), 0)

    def test_validation_warnings(self):
        class InvalidConfigOption(BaseConfigOption):
            def pre_validation(self, config, key_name):
                self.warnings.append('pre_validation warning')

            def run_validation(self, value):
                self.warnings.append('run_validation warning')

            def post_validation(self, config, key_name):
                self.warnings.append('post_validation warning')

        c = base.Config(schema=(('invalid_option', InvalidConfigOption()), ))

        errors, warnings = c.validate()

        self.assertEqual(len(errors), 0)
        self.assertEqual(warnings, [
            ('invalid_option', 'pre_validation warning'),
            ('invalid_option', 'run_validation warning'),
            ('invalid_option', 'post_validation warning'),
        ])

    def test_load_from_file_with_relative_paths(self):
        """
        When explicitly setting a config file, paths should be relative to the
        config file, not the working directory.
        """

        config_dir = TemporaryDirectory()
        config_fname = os.path.join(config_dir.name, 'mkdocs.yml')
        docs_dir = os.path.join(config_dir.name, 'src')
        os.mkdir(docs_dir)

        config_file = open(config_fname, 'w')

        try:
            config_file.write("docs_dir: src\nsite_name: MkDocs Test\n")
            config_file.flush()
            config_file.close()

            cfg = base.load_config(config_file=config_file)
            self.assertTrue(isinstance(cfg, base.Config))
            self.assertEqual(cfg['site_name'], 'MkDocs Test')
            self.assertEqual(cfg['docs_dir'], docs_dir)
            self.assertEqual(cfg.config_file_path, config_fname)
            self.assertIsInstance(cfg.config_file_path, str)
        finally:
            config_dir.cleanup()
