# SPDX-License-Identifier: Apache-2.0
# Copyright Contributors to the Rez Project


"""
test configuration settings
"""
import unittest
from rez.tests.util import TestBase, TempdirMixin, restore_os_environ
from rez.exceptions import ConfigurationError
from rez.config import Config, get_module_root_config, _replace_config, _Deprecation
from rez.system import system
from rez.utils.data_utils import RO_AttrDictWrapper
from rez.packages import get_developer_package
from rez.deprecations import RezDeprecationWarning, warn
import os
import os.path
import subprocess
import functools
import shutil
import unittest.mock


class TestConfig(TestBase):
    @classmethod
    def setUpClass(cls):
        cls.settings = {}
        cls.root_config_file = get_module_root_config()
        cls.config_path = cls.data_path("config")

    def _test_basic(self, c):
        self.assertEqual(type(c.warn_all), bool)
        self.assertEqual(type(c.build_directory), str)

        # plugin settings
        p = c.plugins
        self.assertEqual(type(p.release_hook.emailer), RO_AttrDictWrapper)
        self.assertEqual(type(p.release_hook.emailer.sender), str)
        self.assertEqual(type(p.release_hook.emailer.smtp_port), int)

        # plugin settings common to a plugin type
        self.assertEqual(type(p.release_vcs.tag_name), str)

    def _test_overrides(self, c):
        c.override("debug_none", True)
        c.override("build_directory", "floober")
        c.override("plugins.release_vcs.tag_name", "bah")
        c.override("plugins.release_hook.emailer.sender", "joe.bloggs")

        self.assertEqual(c.debug_none, True)
        self.assertEqual(c.build_directory, "floober")
        self.assertEqual(c.plugins.release_vcs.tag_name, "bah")
        self.assertEqual(c.plugins.release_hook.emailer.sender, "joe.bloggs")

        # second override
        c.override("build_directory", "flabber")
        self.assertEqual(c.build_directory, "flabber")

        # remove override
        value = c.tmpdir or ''
        new_value = value + '_'
        c.override("tmpdir", new_value)
        self.assertEqual(c.tmpdir, new_value)
        c.remove_override("tmpdir")
        value_ = c.tmpdir or ''
        self.assertEqual(value_, value)

        self._test_basic(c)

    def test_1(self):
        """Test just the root config file."""

        # do a full validation of a config
        c = Config([self.root_config_file], locked=True)
        c.validate_data()

        # check a few expected settings
        c = Config([self.root_config_file], locked=True)
        self._test_basic(c)
        self.assertEqual(c.warn_all, False)
        self.assertEqual(c.build_directory, "build")

        # check user path expansion
        self.assertEqual(c.local_packages_path,
                         os.path.expanduser(os.path.join("~", "packages")))

        # check access to plugins settings common to a plugin type
        self.assertEqual(c.plugins.release_vcs.tag_name, '{qualified_name}')

        # check access to plugins settings
        self.assertEqual(c.plugins.release_hook.emailer.smtp_port, 25)

        # check system attribute expansion
        expected_value = "%s@rez-release.com" % system.user
        self.assertEqual(c.plugins.release_hook.emailer.sender, expected_value)

        # check that an env-var override doesn't affect locked config
        os.environ["REZ_WARN_NONE"] = "true"
        self.assertEqual(c.warn_none, False)

        self._test_overrides(c)

    def test_2(self):
        """Test a config with an overriding file."""
        conf = os.path.join(self.config_path, "test1.yaml")
        c = Config([self.root_config_file, conf], locked=True)
        self._test_basic(c)

        # check overrides in test1.yaml are being used
        self.assertEqual(c.warn_all, True)
        self.assertEqual(c.plugins.release_vcs.tag_name, "foo")
        self.assertEqual(c.plugins.release_hook.emailer.sender,
                         "santa.claus")

        self._test_overrides(c)

    def test_3(self):
        """Test environment variable config overrides."""
        c = Config([self.root_config_file], locked=False)

        # test basic env-var override
        os.environ["REZ_WARN_ALL"] = "1"
        self.assertEqual(c.warn_all, True)
        self._test_basic(c)

        # test env-var override that contains a system expansion
        REZ_TMPDIR_ = os.environ.get("REZ_TMPDIR")
        os.environ["REZ_TMPDIR"] = "/tmp/{system.user}"
        expected_value = "/tmp/%s" % system.user
        self.assertEqual(c.tmpdir, expected_value)
        if REZ_TMPDIR_:
            os.environ["REZ_TMPDIR"] = REZ_TMPDIR_
        else:
            del os.environ["REZ_TMPDIR"]
        c._uncache("tmpdir")

        # _test_overrides overrides this value, so here we're making sure
        # that an API override takes precedence over an env-var override
        os.environ["BUILD_DIRECTORY"] = "flaabs"
        self._test_overrides(c)

    def test_4(self):
        """Test package config overrides."""
        conf = os.path.join(self.config_path, "test2.py")
        config2 = Config([self.root_config_file, conf])

        with _replace_config(config2):
            pkg = get_developer_package(self.config_path)
            c = pkg.config
            self._test_basic(c)

            # check overrides from package.py are working
            os.environ["REZ_BUILD_DIRECTORY"] = "foo"  # should have no effect
            self.assertEqual(c.build_directory, "weeble")
            self.assertEqual(c.plugins.release_vcs.tag_name, "tag")

            # check list modification is working
            self.assertEqual(c.release_hooks, ["foo", "bah"])

            # check list modification within plugin settings is working
            self.assertEqual(c.plugins.release_hook.emailer.recipients,
                             ["joe@here.com", "jay@there.com"])

            # check system expansion in package overridden setting works
            expected_value = "%s@somewhere.com" % system.user
            self.assertEqual(c.plugins.release_hook.emailer.sender, expected_value)

            # check env-var expansion in package overridden setting works
            os.environ["FUNK"] = "dude"
            expected_value = ["FOO", "BAH_dude", "EEK"]
            self.assertEqual(c.parent_variables, expected_value)

            self._test_overrides(c)

    def test_5(self):
        """Test misconfigurations."""

        # overrides set to bad types
        overrides = {
            "build_directory": [],
            "plugins": {
                "release_hook": {
                    "emailer": {
                        "recipients": 42
                    }
                }
            }
        }
        c = Config([self.root_config_file], overrides=overrides, locked=False)
        with self.assertRaises(ConfigurationError):
            _ = c.build_directory
        with self.assertRaises(ConfigurationError):
            _ = c.plugins.release_hook.emailer.recipients

        # missing keys
        conf = os.path.join(self.config_path, "test1.yaml")
        c = Config([conf], locked=True)

        with self.assertRaises(ConfigurationError):
            _ = c.debug_all  # noqa

    def test_6(self):
        """Test setting of dict values from environ"""
        from rez.config import Dict
        from rez.vendor.schema.schema import Schema

        class TestConfig(Config):
            schema = Schema({
                'dumb_dict': Dict,
            })

            DEFAULT_DUMB_DICT = {'default': 'value'}

            # don't want to bother writing a file just to set a default value,
            # and can't use overrides, as that will make it ignore env vars...
            @property
            def _data(self):
                return {'dumb_dict': self.DEFAULT_DUMB_DICT}

        # need to NOT use locked, because we want to test setting from env
        # vars, but don't want values from "real" os.environ to pollute our
        # test...
        old_environ = os.environ
        try:
            os.environ = {}
            c = TestConfig([])
            self.assertEqual(c.dumb_dict, TestConfig.DEFAULT_DUMB_DICT)

            os.environ = {'REZ_DUMB_DICT': 'foo:bar,more:stuff'}
            c = TestConfig([])
            self.assertEqual(c.dumb_dict, {'foo': 'bar', 'more': 'stuff'})
        finally:
            os.environ = old_environ

    def test_7(self):
        """Test path list environment variable with whitespace."""
        c = Config([self.root_config_file], locked=False)

        # test basic env-var override
        packages_path = [
            "/foo bar/baz",
            "/foo bar/baz hey",
            "/home/foo bar/baz",
        ]
        os.environ["REZ_PACKAGES_PATH"] = os.pathsep.join(packages_path)

        self.assertEqual(c.packages_path, packages_path)

    def test_8(self):
        """Test CLI dict/list value JSON round trip."""
        import json
        import rez.cli._main

        cli_file = rez.cli._main.__file__

        # python /path/to/rez/cli/_main.py config ...
        config_args = [os.sys.executable, cli_file, "config"]

        c = Config([self.root_config_file], locked=True)
        env = {
            key: value
            for key, value in os.environ.items()
            if not key.startswith("REZ_")
        }

        env.update({
            "REZ_DISABLE_HOME_CONFIG": "1"
        })

        test_configs = {
            "packages_path": ["/foo bar/baz", "/foo bar/baz hey", "/home/foo bar/baz"],
            "platform_map": {"foo": {"bar": "baz"}},
        }
        for config_key, test_value in test_configs.items():
            try:
                # Test fetching default value
                stdout = subprocess.check_output(
                    config_args + ["--json", config_key],
                    env=env,
                    text=True,
                )
                self.assertEqual(
                    stdout.strip(),
                    json.dumps(getattr(c, config_key)),
                )

                # Test setting via env var and fetching custom value
                test_json_value = json.dumps(test_value)
                env["REZ_%s_JSON" % config_key.upper()] = test_json_value
                stdout = subprocess.check_output(
                    config_args + ["--json", config_key],
                    env=env,
                )
                self.assertEqual(stdout.decode().strip(), test_json_value)
            except subprocess.CalledProcessError as error:
                print(error.stdout)
                raise


class TestDeprecations(TestBase, TempdirMixin):
    @classmethod
    def setUpClass(cls):
        cls.settings = {}
        TempdirMixin.setUpClass()

    @classmethod
    def tearDownClass(cls):
        TempdirMixin.tearDownClass()

    def test_deprecation_from_user_config(self):
        user_home = os.path.join(self.root, "user_home")
        self.addCleanup(functools.partial(shutil.rmtree, user_home))

        os.makedirs(user_home)

        with open(os.path.join(user_home, ".rezconfig.py"), "w") as fd:
            fd.write("packages_path = ['/tmp/asd']")

        fake_deprecated_settings = {
            "packages_path": _Deprecation("0.0.0"),
        }

        with unittest.mock.patch(
            "rez.config._deprecated_settings",
            fake_deprecated_settings
        ):
            with restore_os_environ():
                os.environ["HOME"] = user_home
                # On Windows, os.path.expanduser will read HOME and then USERPROFILE with Python 3.7.
                # https://docs.python.org/3.7/library/os.path.html#os.path.expanduser
                # Also on Windows but for Python 3.8+, it will look for USERPROFILE and then HOME.
                # https://docs.python.org/3.8/library/os.path.html#os.path.expanduser
                os.environ["USERPROFILE"] = user_home
                config = Config._create_main_config()
                with self.assertWarns(RezDeprecationWarning) as warning:
                    _ = config.data
                    # Assert just to ensure the test was set up properly.
                    self.assertEqual(config.data["packages_path"], ["/tmp/asd"])

                self.assertEqual(
                    str(warning.warning),
                    "config setting named 'packages_path' is deprecated and will be removed in 0.0.0.",
                )

    def test_deprecation_from_env_var(self):
        fake_deprecated_settings = {
            "packages_path": _Deprecation("0.0.0"),
        }

        with unittest.mock.patch(
            "rez.config._deprecated_settings",
            fake_deprecated_settings
        ):
            with restore_os_environ():
                # Test with non-json env var
                os.environ["REZ_PACKAGES_PATH"] = "/tmp/asd2"
                os.environ["REZ_DISABLE_HOME_CONFIG"] = "1"
                config = Config._create_main_config()
                with self.assertWarns(RezDeprecationWarning) as warning:
                    _ = config.data
                    # Assert just to ensure the test was set up properly.
                    self.assertEqual(config.data["packages_path"], ["/tmp/asd2"])

                self.assertEqual(
                    str(warning.warning),
                    "config setting named 'packages_path' (configured through the "
                    "REZ_PACKAGES_PATH environment variable) is deprecated and will "
                    "be removed in 0.0.0.",
                )

            with restore_os_environ():
                # Test with json env var
                os.environ["REZ_PACKAGES_PATH_JSON"] = '["/tmp/asd2"]'
                os.environ["REZ_DISABLE_HOME_CONFIG"] = "1"
                config = Config._create_main_config()
                with self.assertWarns(RezDeprecationWarning) as warning:
                    _ = config.data
                    # Assert just to ensure the test was set up properly.
                    self.assertEqual(config.data["packages_path"], ["/tmp/asd2"])

                self.assertEqual(
                    str(warning.warning),
                    "config setting named 'packages_path' (configured through the "
                    "REZ_PACKAGES_PATH_JSON environment variable) is deprecated and will "
                    "be removed in 0.0.0.",
                )

    def test_non_preformatted_warning(self):
        with self.assertWarns(DeprecationWarning) as warning:
            warn('Warning Message', DeprecationWarning, pre_formatted=False)
        self.assertEqual(str(warning.warning), 'Warning Message')


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