# Copyright 2008-2015 Canonical Ltd.  All rights reserved.
#
# This file is part of lazr.config.
#
# lazr.config is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# lazr.config is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
# License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with lazr.config.  If not, see <http://www.gnu.org/licenses/>.

"""Tests of lazr.config."""

__all__ = [
    "TestConfig",
]


import importlib.util
import unittest
from configparser import MissingSectionHeaderError, NoSectionError
from io import StringIO
from operator import attrgetter
from pathlib import Path

from zope.interface.exceptions import DoesNotImplement
from zope.interface.verify import verifyObject

from lazr.config import ConfigSchema, ImplicitTypeSchema
from lazr.config.interfaces import (
    ConfigErrors,
    InvalidSectionNameError,
    IStackableConfig,
    NoCategoryError,
    NoConfigError,
    RedefinedSectionError,
    UnknownKeyError,
    UnknownSectionError,
)


class TestConfig(unittest.TestCase):
    def _testfile(self, conf_file):
        spec = importlib.util.find_spec("lazr.config.tests.testdata")
        return str(Path(spec.origin).parent / conf_file)

    def test_missing_category(self):
        schema = ConfigSchema(self._testfile("base.conf"))
        self.assertRaises(NoCategoryError, schema.getByCategory, "non-section")

    def test_missing_file(self):
        self.assertRaises(IOError, ConfigSchema, "/does/not/exist")

    def test_must_be_ascii(self):
        self.assertRaises(
            UnicodeError, ConfigSchema, self._testfile("bad-nonascii.conf")
        )

    def test_missing_schema_section(self):
        schema = ConfigSchema(self._testfile("base.conf"))
        self.assertRaises(NoSectionError, schema.__getitem__, "section-4")

    def test_missing_header_section(self):
        self.assertRaises(
            MissingSectionHeaderError,
            ConfigSchema,
            self._testfile("bad-sectionless.conf"),
        )

    def test_redefined_section(self):
        self.assertRaises(
            RedefinedSectionError,
            ConfigSchema,
            self._testfile("bad-redefined-section.conf"),
        )
        # XXX sinzui 2007-12-13:
        # ConfigSchema should raise RedefinedKeyError when a section redefines
        # a key.

    def test_invalid_section_name(self):
        self.assertRaises(
            InvalidSectionNameError,
            ConfigSchema,
            self._testfile("bad-invalid-name.conf"),
        )

    def test_invalid_characters(self):
        self.assertRaises(
            InvalidSectionNameError,
            ConfigSchema,
            self._testfile("bad-invalid-name-chars.conf"),
        )

    def test_load_missing_file(self):
        schema = ConfigSchema(self._testfile("base.conf"))
        self.assertRaises(IOError, schema.load, "/no/such/file.conf")

    def test_no_name_argument(self):
        config = """
[meta]
metakey: unsupported
[unknown-section]
key1 = value1
[section_1]
keyn: unknown key
key1: bad character in caf\xc3)
[section_3.template]
key1: schema suffixes are not permitted
"""
        schema = ConfigSchema(self._testfile("base.conf"))
        self.assertRaises(AttributeError, schema.loadFile, StringIO(config))

    def test_missing_section(self):
        schema = ConfigSchema(self._testfile("base.conf"))
        config = schema.load(self._testfile("local.conf"))
        self.assertRaises(NoSectionError, config.__getitem__, "section-4")

    def test_undeclared_optional_section(self):
        schema = ConfigSchema(self._testfile("base.conf"))
        config = schema.load(self._testfile("local.conf"))
        self.assertRaises(
            NoSectionError, config.__getitem__, "section_3.app_a"
        )

    def test_nonexistent_category_name(self):
        schema = ConfigSchema(self._testfile("base.conf"))
        config = schema.load(self._testfile("local.conf"))
        self.assertRaises(NoCategoryError, config.getByCategory, "non-section")

    def test_all_config_errors(self):
        schema = ConfigSchema(self._testfile("base.conf"))
        config = schema.loadFile(
            StringIO("""
[meta]
metakey: unsupported
[unknown-section]
key1 = value1
[section_1]
keyn: unknown key
key1: bad character in caf\xc3)
[section_3.template]
key1: schema suffixes are not permitted
"""),
            "bad config",
        )
        try:
            config.validate()
        except ConfigErrors as errors:
            sorted_errors = sorted(
                errors.errors, key=attrgetter("__class__.__name__")
            )
            self.assertEqual(
                str(errors), "ConfigErrors: bad config is not valid."
            )
        else:
            self.fail("ConfigErrors expected")
        self.assertEqual(len(sorted_errors), 4)
        self.assertEqual(
            [error.__class__ for error in sorted_errors],
            [
                UnicodeEncodeError,
                UnknownKeyError,
                UnknownKeyError,
                UnknownSectionError,
            ],
        )

    def test_not_stackable(self):
        try:
            from zope.interface.exceptions import MultipleInvalid
        except ImportError:  # zope.interface < 5.0.0

            class MultipleInvalid(Exception):
                pass

        schema = ConfigSchema(self._testfile("base.conf"))
        config = schema.load(self._testfile("local.conf"))
        try:
            verifyObject(IStackableConfig, config.extends)
        except DoesNotImplement:
            pass
        except MultipleInvalid as e:
            if not any(
                isinstance(invalid, DoesNotImplement)
                for invalid in e.exceptions
            ):
                self.fail("MultipleInvalid raised without DoesNotImplement")
        else:
            self.fail("DoesNotImplement not raised")

    def test_bad_pop(self):
        schema = ConfigSchema(self._testfile("base.conf"))
        config = schema.load(self._testfile("local.conf"))
        config.push("one", "")
        config.push("two", "")
        self.assertRaises(NoConfigError, config.pop, "bad-name")

    def test_cannot_pop_bottom(self):
        schema = ConfigSchema(self._testfile("base.conf"))
        config = schema.load(self._testfile("local.conf"))
        config.pop("local.conf")
        self.assertRaises(NoConfigError, config.pop, "base.conf")

    def test_multiline_preserves_indentation(self):
        schema = ImplicitTypeSchema(self._testfile("base.conf"))
        config = schema.load(self._testfile("local.conf"))
        convert = config["section_1"]._convert
        orig = """\
multiline value 1
    multiline value 2"""
        new = convert(orig)
        self.assertMultiLineEqual(new, orig)

    def test_multiline_strips_leading_and_trailing_whitespace(self):
        schema = ImplicitTypeSchema(self._testfile("base.conf"))
        config = schema.load(self._testfile("local.conf"))
        convert = config["section_1"]._convert
        orig = """
    multiline value 1
    multiline value 2
    """
        new = convert(orig)
        self.assertMultiLineEqual(new, orig.strip())

    def test_multiline_key(self):
        schema = ImplicitTypeSchema(self._testfile("base.conf"))
        config = schema.load(self._testfile("local.conf"))
        self.assertMultiLineEqual(
            config["section_33"].key2,
            """\
multiline value 1
multiline value 2""",
        )

    def test_lp1397779(self):
        # Fix DuplicateSectionErrors when you .push() a config that has a
        # section already defined in the config.
        schema = ConfigSchema(self._testfile("base.conf"))
        config = schema.load(self._testfile("local.conf"))
        self.assertEqual(config["section_1"]["key1"], "foo")
        config.push(
            "dupsec",
            """\
[section_1]
key1: baz
[section_1]
key1: qux
""",
        )
        self.assertEqual(config["section_1"]["key1"], "qux")
