# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.
import sys
import unittest
from pathlib import Path

from mozfile.mozfile import NamedTemporaryFile
from mozunit import main

from mach.config import (
    BooleanType,
    ConfigException,
    ConfigSettings,
    IntegerType,
    PathType,
    PositiveIntegerType,
    StringType,
)
from mach.decorators import SettingsProvider

CONFIG1 = r"""
[foo]

bar = bar_value
baz = /baz/foo.c
"""

CONFIG2 = r"""
[foo]

bar = value2
"""


@SettingsProvider
class Provider1:
    config_settings = [
        ("foo.bar", StringType, "desc"),
        ("foo.baz", PathType, "desc"),
    ]


@SettingsProvider
class ProviderDuplicate:
    config_settings = [
        ("dupesect.foo", StringType, "desc"),
        ("dupesect.foo", StringType, "desc"),
    ]


@SettingsProvider
class Provider2:
    config_settings = [
        ("a.string", StringType, "desc"),
        ("a.boolean", BooleanType, "desc"),
        ("a.pos_int", PositiveIntegerType, "desc"),
        ("a.int", IntegerType, "desc"),
        ("a.path", PathType, "desc"),
    ]


@SettingsProvider
class Provider3:
    @classmethod
    def config_settings(cls):
        return [
            ("a.string", "string", "desc"),
            ("a.boolean", "boolean", "desc"),
            ("a.pos_int", "pos_int", "desc"),
            ("a.int", "int", "desc"),
            ("a.path", "path", "desc"),
        ]


@SettingsProvider
class Provider4:
    config_settings = [
        ("foo.abc", StringType, "desc", "a", {"choices": set("abc")}),
        ("foo.xyz", StringType, "desc", "w", {"choices": set("xyz")}),
    ]


@SettingsProvider
class Provider5:
    config_settings = [
        ("foo.*", "string", "desc"),
        ("foo.bar", "string", "desc"),
    ]


class TestConfigSettings(unittest.TestCase):
    def test_empty(self):
        s = ConfigSettings()

        self.assertEqual(len(s), 0)
        self.assertNotIn("foo", s)

    def test_duplicate_option(self):
        s = ConfigSettings()

        with self.assertRaises(ConfigException):
            s.register_provider(ProviderDuplicate)

    def test_simple(self):
        s = ConfigSettings()
        s.register_provider(Provider1)

        self.assertEqual(len(s), 1)
        self.assertIn("foo", s)

        foo = s["foo"]
        foo = s.foo

        self.assertEqual(len(foo), 0)
        self.assertEqual(len(foo._settings), 2)

        self.assertIn("bar", foo._settings)
        self.assertIn("baz", foo._settings)

        self.assertNotIn("bar", foo)
        foo["bar"] = "value1"
        self.assertIn("bar", foo)

        self.assertEqual(foo["bar"], "value1")
        self.assertEqual(foo.bar, "value1")

    def test_assignment_validation(self):
        s = ConfigSettings()
        s.register_provider(Provider2)

        a = s.a

        # Assigning an undeclared setting raises.
        exc_type = AttributeError if sys.version_info < (3, 0) else KeyError
        with self.assertRaises(exc_type):
            a.undefined = True

        with self.assertRaises(KeyError):
            a["undefined"] = True

        # Basic type validation.
        a.string = "foo"
        a.string = "foo"

        with self.assertRaises(TypeError):
            a.string = False

        a.boolean = True
        a.boolean = False

        with self.assertRaises(TypeError):
            a.boolean = "foo"

        a.pos_int = 5
        a.pos_int = 0

        with self.assertRaises(ValueError):
            a.pos_int = -1

        with self.assertRaises(TypeError):
            a.pos_int = "foo"

        a.int = 5
        a.int = 0
        a.int = -5

        with self.assertRaises(TypeError):
            a.int = 1.24

        with self.assertRaises(TypeError):
            a.int = "foo"

        a.path = "/home/gps"
        a.path = "foo.c"
        a.path = "foo/bar"
        a.path = "./foo"

    def retrieval_type_helper(self, provider):
        s = ConfigSettings()
        s.register_provider(provider)

        a = s.a

        a.string = "foo"
        a.boolean = True
        a.pos_int = 12
        a.int = -4
        a.path = "./foo/bar"

        self.assertIsInstance(a.string, (str,))
        self.assertIsInstance(a.boolean, bool)
        self.assertIsInstance(a.pos_int, int)
        self.assertIsInstance(a.int, int)
        self.assertIsInstance(a.path, (str,))

    def test_retrieval_type(self):
        self.retrieval_type_helper(Provider2)
        self.retrieval_type_helper(Provider3)

    def test_choices_validation(self):
        s = ConfigSettings()
        s.register_provider(Provider4)

        foo = s.foo
        foo.abc
        with self.assertRaises(ValueError):
            foo.xyz

        with self.assertRaises(ValueError):
            foo.abc = "e"

        foo.abc = "b"
        foo.xyz = "y"

    def test_wildcard_options(self):
        s = ConfigSettings()
        s.register_provider(Provider5)

        foo = s.foo

        self.assertIn("*", foo._settings)
        self.assertNotIn("*", foo)

        foo.baz = "value1"
        foo.bar = "value2"

        self.assertIn("baz", foo)
        self.assertEqual(foo.baz, "value1")

        self.assertIn("bar", foo)
        self.assertEqual(foo.bar, "value2")

    def test_file_reading_single(self):
        temp = NamedTemporaryFile(mode="wt")
        temp.write(CONFIG1)
        temp.flush()

        s = ConfigSettings()
        s.register_provider(Provider1)

        s.load_file(Path(temp.name))

        self.assertEqual(s.foo.bar, "bar_value")

    def test_file_reading_multiple(self):
        """Loading multiple files has proper overwrite behavior."""
        temp1 = NamedTemporaryFile(mode="wt")
        temp1.write(CONFIG1)
        temp1.flush()

        temp2 = NamedTemporaryFile(mode="wt")
        temp2.write(CONFIG2)
        temp2.flush()

        s = ConfigSettings()
        s.register_provider(Provider1)

        s.load_files([Path(temp1.name), Path(temp2.name)])

        self.assertEqual(s.foo.bar, "value2")

    def test_file_reading_missing(self):
        """Missing files should silently be ignored."""

        s = ConfigSettings()

        s.load_file("/tmp/foo.ini")

    def test_file_writing(self):
        s = ConfigSettings()
        s.register_provider(Provider2)

        s.a.string = "foo"
        s.a.boolean = False

        temp = NamedTemporaryFile("wt")
        s.write(temp)
        temp.flush()

        s2 = ConfigSettings()
        s2.register_provider(Provider2)

        s2.load_file(temp.name)

        self.assertEqual(s.a.string, s2.a.string)
        self.assertEqual(s.a.boolean, s2.a.boolean)


if __name__ == "__main__":
    main()
