#!/usr/bin/env python3
# $Id: test_settings.py 9906 2024-08-15 08:43:38Z grubert $
# Author: David Goodger <goodger@python.org>
# Copyright: This module has been placed in the public domain.

"""
Tests of runtime settings.
"""

import os
import difflib
import warnings
from pathlib import Path
import sys
import unittest

if __name__ == '__main__':
    # prepend the "docutils root" to the Python library path
    # so we import the local `docutils` package.
    sys.path.insert(0, str(Path(__file__).resolve().parents[1]))

from docutils import frontend, utils
from docutils.writers import pep_html, html5_polyglot
from docutils.parsers import rst

# DATA_ROOT is ./test/data/ from the docutils root
DATA_ROOT = os.path.abspath(os.path.join(__file__, '..', 'data'))


def fixpath(path):
    return os.path.join(DATA_ROOT, path)


class ConfigFileTests(unittest.TestCase):

    config_files = {'old': fixpath('config_old.rst'),
                    'one': fixpath('config_1.rst'),
                    'two': fixpath('config_2.rst'),
                    'list': fixpath('config_list.rst'),
                    'list2': fixpath('config_list_2.rst'),
                    'error': fixpath('config_encoding.rst'),
                    'error2': fixpath('config_encoding_2.rst'),
                    'syntax_error': fixpath('config_syntax_error.rst'),
                    }

    # expected settings after parsing the equally named config_file:
    settings = {
        'old': {'datestamp': '%Y-%m-%d %H:%M UTC',
                'generator': True,
                'no_random': True,
                'python_home': 'http://www.python.org',
                'source_link': True,
                'stylesheet': None,
                'stylesheet_path': ['stylesheets/pep.css'],
                'template': fixpath('pep-html-template'),
                },
        'one': {'datestamp': '%Y-%m-%d %H:%M UTC',
                'generator': True,
                'no_random': True,
                'python_home': 'http://www.python.org',
                'raw_enabled': False,
                'record_dependencies': utils.DependencyList(),
                'source_link': True,
                'stylesheet': None,
                'stylesheet_path': ['stylesheets/pep.css'],
                'tab_width': 8,
                'template': fixpath('pep-html-template'),
                'trim_footnote_reference_space': True,
                'output_encoding': 'ascii',
                'output_encoding_error_handler': 'xmlcharrefreplace',
                },
        'two': {'footnote_references': 'superscript',
                'generator': False,
                'record_dependencies': utils.DependencyList(),
                'stylesheet': None,
                'stylesheet_path': ['test.css'],
                'trim_footnote_reference_space': None,
                'output_encoding_error_handler': 'namereplace',
                },
        'two (html5)': {
                # use defaults from html5_polyglot writer component
                # ignore settings in [html4css1 writer] section,
                'generator': True,
                'raw_enabled': False,
                'record_dependencies': utils.DependencyList(),
                'source_link': False,
                'tab_width': 8,
                'trim_footnote_reference_space': True,
                'output_encoding_error_handler': 'namereplace',
                },
        'list': {'expose_internals': ['a', 'b', 'c', 'd', 'e'],
                 'smartquotes_locales': [('de', '«»‹›')],
                 'strip_classes': ['spam', 'pan', 'fun', 'parrot'],
                 'strip_elements_with_classes': ['sugar', 'flour', 'milk',
                                                 'safran']
                 },
        'list2': {'expose_internals': ['a', 'b', 'c', 'd', 'e', 'f'],
                  'smartquotes_locales': [('de', '«»‹›'),
                                          ('nl', '„”’’'),
                                          ('cs', '»«›‹'),
                                          ('fr', ['« ', ' »', '‹ ', ' ›'])
                                          ],
                  'strip_classes': ['spam', 'pan', 'fun', 'parrot',
                                    'ham', 'eggs'],
                  'strip_elements_with_classes': ['sugar', 'flour', 'milk',
                                                  'safran', 'eggs', 'salt'],
                  'stylesheet': ['style2.css', 'style3.css'],
                  'stylesheet_path': None,
                  },
        'error': {'error_encoding': 'ascii',
                  'error_encoding_error_handler': 'strict'},
        'error2': {'error_encoding': 'latin1'},
        }

    compare = difflib.Differ().compare
    """Comparison method shared by all tests."""

    def setUp(self):
        warnings.filterwarnings('ignore',
                                category=frontend.ConfigDeprecationWarning)
        warnings.filterwarnings('ignore', category=DeprecationWarning)
        self.option_parser = frontend.OptionParser(
            components=(pep_html.Writer, rst.Parser), read_config_files=None)

    def files_settings(self, *names):
        settings = frontend.Values()
        for name in names:
            cfs = self.option_parser.get_config_file_settings(
                                                    self.config_files[name])
            settings.update(cfs, self.option_parser)
        return settings.__dict__

    def expected_settings(self, *names):
        expected = {}
        for name in names:
            expected.update(self.settings[name])
        return expected

    def compare_output(self, result, expected):
        """`result` and `expected` should both be dicts."""
        self.assertIn('record_dependencies', result)
        rd_result = result.pop('record_dependencies')
        rd_expected = expected.pop('record_dependencies', None)
        if rd_expected is not None:
            self.assertEqual(str(rd_result), str(rd_expected))
        self.assertEqual(expected, result)

    def test_nofiles(self):
        self.compare_output(self.files_settings(),
                            self.expected_settings())

    def test_old(self):
        with self.assertWarnsRegex(FutureWarning,
                                   r'The "\[option\]" section is deprecated.'):
            self.files_settings('old')

    def test_syntax_error(self):
        with self.assertRaisesRegex(
                 ValueError,
                 'Error in config file ".*config_syntax_error.rst", '
                 r'section "\[general\]"'):
            self.files_settings('syntax_error')

    def test_one(self):
        self.compare_output(self.files_settings('one'),
                            self.expected_settings('one'))

    def test_multiple(self):
        self.compare_output(self.files_settings('one', 'two'),
                            self.expected_settings('one', 'two'))

    def test_multiple_with_html5_writer(self):
        # initialize option parser with different component set
        self.option_parser = frontend.OptionParser(
            components=(html5_polyglot.Writer, rst.Parser),
            read_config_files=None)
        # generator setting not changed by "config_2.rst":
        self.compare_output(self.files_settings('one', 'two'),
                            self.expected_settings('two (html5)'))

    def test_old_and_new(self):
        self.compare_output(self.files_settings('old', 'two'),
                            self.expected_settings('old', 'two'))

    def test_list(self):
        self.compare_output(self.files_settings('list'),
                            self.expected_settings('list'))

    def test_list2(self):
        # setting `stylesheet` in 'list2' resets stylesheet_path to None
        self.compare_output(self.files_settings('list', 'list2'),
                            self.expected_settings('list2'))

    def test_encoding_error_handler(self):
        # set error_encoding and error_encoding_error_handler (from affix)
        self.compare_output(self.files_settings('error'),
                            self.expected_settings('error'))

    def test_encoding_error_handler2(self):
        # second config file only changes encoding, not error_handler:
        self.compare_output(self.files_settings('error', 'error2'),
                            self.expected_settings('error', 'error2'))


class ConfigEnvVarFileTests(ConfigFileTests):

    """
    Repeats the tests of `ConfigFileTests` using the ``DOCUTILSCONFIG``
    environment variable and the standard Docutils config file mechanism.
    """

    def setUp(self):
        ConfigFileTests.setUp(self)
        self.orig_environ = os.environ
        os.environ = os.environ.copy()

    def files_settings(self, *names):
        files = [self.config_files[name] for name in names]
        os.environ['DOCUTILSCONFIG'] = os.pathsep.join(files)
        settings = self.option_parser.get_standard_config_settings()
        return settings.__dict__

    def tearDown(self):
        os.environ = self.orig_environ

    def test_old(self):
        pass  # don't repreat this test

    @unittest.skipUnless(
        os.name == 'posix',
        'os.path.expanduser() does not use HOME on Windows (since 3.8)')
    def test_get_standard_config_files(self):
        os.environ['HOME'] = '/home/parrot'
        # TODO: set up mock home directory under Windows
        self.assertEqual(self.option_parser.get_standard_config_files(),
                         ['/etc/docutils.conf',
                          './docutils.conf',
                          '/home/parrot/.docutils'])
        # split at ':', expand leading '~':
        os.environ['DOCUTILSCONFIG'] = ('/etc/docutils2.conf'
                                        ':~/.config/docutils.conf')
        self.assertEqual(self.option_parser.get_standard_config_files(),
                         ['/etc/docutils2.conf',
                          '/home/parrot/.config/docutils.conf'])


class HelperFunctionsTests(unittest.TestCase):

    pathdict = {'foo': 'hallo', 'ham': 'häm', 'spam': 'spam'}
    keys = ['foo', 'ham']

    def setUp(self):
        with warnings.catch_warnings():
            warnings.filterwarnings('ignore', category=DeprecationWarning)
            self.option_parser = frontend.OptionParser(
                components=(rst.Parser,), read_config_files=None)

    def test_make_paths_absolute(self):
        pathdict = self.pathdict.copy()
        frontend.make_paths_absolute(pathdict, self.keys, base_path='base')
        self.assertEqual(pathdict['foo'], os.path.abspath('base/hallo'))
        self.assertEqual(pathdict['ham'], os.path.abspath('base/häm'))
        # not touched, because key not in keys:
        self.assertEqual(pathdict['spam'], 'spam')

    def test_make_paths_absolute_cwd(self):
        # With base_path None, the cwd is used as base path.
        # Settings values may-be `unicode` instances, therefore
        # os.getcwdu() is used and the converted path is a unicode instance:
        pathdict = self.pathdict.copy()
        frontend.make_paths_absolute(pathdict, self.keys)
        self.assertEqual(pathdict['foo'], os.path.abspath('hallo'))
        self.assertEqual(pathdict['ham'], os.path.abspath('häm'))
        # not touched, because key not in keys:
        self.assertEqual(pathdict['spam'], 'spam')

    boolean_settings = (
                (True, True),
                ('1', True),
                ('on', True),
                ('yes', True),
                ('true', True),
                ('0', False),
                ('off', False),
                ('no', False),
                ('false', False),
               )

    def test_validate_boolean(self):
        for v, result in self.boolean_settings:
            self.assertEqual(frontend.validate_boolean(v), result)

    def test_validate_ternary(self):
        tests = (
                 ('500V', '500V'),
                 ('parrot', 'parrot'),
                )
        for v, result in self.boolean_settings + tests:
            self.assertEqual(frontend.validate_ternary(v), result)

    def test_validate_threshold(self):
        tests = (('1', 1),
                 ('info', 1),
                 ('warning', 2),
                 ('error', 3),
                 ('severe', 4),
                 ('none', 5),
                 )
        for v, result in tests:
            self.assertEqual(
                frontend.validate_threshold(v), result)
        with self.assertRaisesRegex(LookupError, "unknown threshold: 'debug'"):
            frontend.validate_threshold('debug')

    def test_validate_colon_separated_string_list(self):
        tests = (('a', ['a']),
                 ('a:b', ['a', 'b']),
                 (['a'], ['a']),
                 (['a', 'b:c'], ['a', 'b', 'c']),
                 )
        for v, result in tests:
            self.assertEqual(
                frontend.validate_colon_separated_string_list(v), result)

    def test_validate_comma_separated_list(self):
        tests = (('a', ['a']),
                 ('a,b', ['a', 'b']),
                 (['a'], ['a']),
                 (['a', 'b,c'], ['a', 'b', 'c']),
                 )
        for v, result in tests:
            self.assertEqual(frontend.validate_comma_separated_list(v), result)

    def test_validate_math_output(self):
        tests = (('', ()),
                 ('LaTeX ', ('latex', '')),
                 ('MathML', ('mathml', '')),
                 ('MathML  PanDoc', ('mathml', 'pandoc')),
                 ('HTML  math.css, X.css', ('html', 'math.css, X.css')),
                 ('MathJax  /MathJax.js', ('mathjax', '/MathJax.js')),
                 )
        for v, result in tests:
            self.assertEqual(frontend.validate_math_output(v), result)

    def test_validate_math_output_errors(self):
        tests = (('XML', 'Unknown math output format: "XML",\n'
                  "    choose from ('html', 'latex', 'mathml', 'mathjax')."),
                 ('MathML blame', 'MathML converter "blame" not supported,\n'
                  "    choose from ('', 'latexml', 'ttm', 'blahtexml', "
                  "'pandoc')."),
                 )
        for value, message in tests:
            with self.assertRaises(LookupError) as cm:
                frontend.validate_math_output(value)
            self.assertEqual(message, str(cm.exception))

    def test_validate_url_trailing_slash(self):
        tests = (('', './'),
                 (None, './'),
                 ('http://example.org', 'http://example.org/'),
                 ('http://example.org/', 'http://example.org/'),
                 )
        for v, result in tests:
            self.assertEqual(frontend.validate_url_trailing_slash(v), result)

    def test_validate_smartquotes_locales(self):
        tests = (
            ('en:ssvv', [('en', 'ssvv')]),
            ('sd:«»°°', [('sd', '«»°°')]),
            ([('sd', '«»°°'), 'ds:°°«»'], [('sd', '«»°°'), ('ds', '°°«»')]),
            ('frs:« : »:((:))', [('frs', ['« ', ' »', '((', '))'])]),
            )
        for v, result in tests:
            self.assertEqual(frontend.validate_smartquotes_locales(v), result)


if __name__ == '__main__':
    unittest.main()
