# Copyright 2014 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).

import os
import unittest
from functools import partial
from textwrap import dedent
from typing import Dict, List, Optional

from pants.base.build_environment import get_buildroot
from pants.option.option_value_container import OptionValueContainer
from pants.option.options_bootstrapper import OptionsBootstrapper
from pants.option.scope import ScopeInfo
from pants.util.contextutil import temporary_dir, temporary_file, temporary_file_path
from pants.util.logging import LogLevel


class OptionsBootstrapperTest(unittest.TestCase):
    @staticmethod
    def _config_path(path: Optional[str]) -> List[str]:
        if path is None:
            return ["--pants-config-files=[]"]
        return [f"--pants-config-files=['{path}']"]

    def assert_bootstrap_options(
        self,
        *,
        config: Optional[Dict[str, str]] = None,
        env: Optional[Dict[str, str]] = None,
        args: Optional[List[str]] = None,
        **expected_entries,
    ) -> None:
        with temporary_file(binary_mode=False) as fp:
            fp.write("[DEFAULT]\n")
            if config:
                for k, v in config.items():
                    fp.write(f"{k} = {repr(v)}\n")
            fp.close()

            args = [*self._config_path(fp.name), *(args or [])]
            bootstrapper = OptionsBootstrapper.create(env=env or {}, args=args, allow_pantsrc=False)
            vals = bootstrapper.get_bootstrap_options().for_global_scope()

        vals_dict = {k: getattr(vals, k) for k in expected_entries}
        self.assertEqual(expected_entries, vals_dict)

    def test_bootstrap_seed_values(self) -> None:
        def assert_seed_values(
            *,
            config: Optional[Dict[str, str]] = None,
            env: Optional[Dict[str, str]] = None,
            args: Optional[List[str]] = None,
            workdir: Optional[str] = None,
            distdir: Optional[str] = None,
        ) -> None:
            self.assert_bootstrap_options(
                config=config,
                env=env,
                args=args,
                pants_workdir=workdir or os.path.join(get_buildroot(), ".pants.d"),
                pants_distdir=distdir or os.path.join(get_buildroot(), "dist"),
            )

        # Check for valid default seed values
        assert_seed_values()

        # Check getting values from config, env and args.
        assert_seed_values(
            config={"pants_workdir": "/from_config/.pants.d"},
            workdir="/from_config/.pants.d",
        )
        assert_seed_values(args=["--pants-distdir=/from_args/dist"], distdir="/from_args/dist")

        # Check that args > env > config.
        assert_seed_values(
            config={
                "pants_workdir": "/from_config/.pants.d",
                "pants_distdir": "/from_config/dist",
            },
            args=["--pants-distdir=/from_args/dist"],
            workdir="/from_config/.pants.d",
            distdir="/from_args/dist",
        )

        # Check that unrelated args and config don't confuse us.
        assert_seed_values(
            config={
                "pants_workdir": "/from_config/.pants.d",
                "pants_distdir": "/from_config/dist",
                "unrelated": "foo",
            },
            env={
                "PANTS_DISTDIR": "/from_env/dist",
                "PANTS_NO_RELATIONSHIP": "foo",
            },
            args=["--pants-distdir=/from_args/dist", "--foo=bar", "--baz"],
            workdir="/from_config/.pants.d",
            distdir="/from_args/dist",
        )

    def test_bootstrap_bool_option_values(self) -> None:
        # Check the default.
        self.assert_bootstrap_options(pantsrc=True)

        assert_pantsrc_is_false = partial(self.assert_bootstrap_options, pantsrc=False)
        assert_pantsrc_is_false(args=["--no-pantsrc"])
        assert_pantsrc_is_false(config={"pantsrc": "false"})
        assert_pantsrc_is_false(env={"PANTS_PANTSRC": "False"})

    def test_create_bootstrapped_options(self) -> None:
        # Check that we can set a bootstrap option from a cmd-line flag and have that interpolate
        # correctly into regular config.
        with temporary_file(binary_mode=False) as fp:
            fp.write(
                dedent(
                    """
                    [foo]
                    bar = "%(pants_workdir)s/baz"

                    [fruit]
                    apple = "%(pants_distdir)s/banana"
                    """
                )
            )
            fp.close()
            args = ["--pants-workdir=/qux"] + self._config_path(fp.name)
            bootstrapper = OptionsBootstrapper.create(
                env={"PANTS_DISTDIR": "/pear"}, args=args, allow_pantsrc=False
            )
            opts = bootstrapper.full_options_for_scopes(
                known_scope_infos=[
                    ScopeInfo(""),
                    ScopeInfo("foo"),
                    ScopeInfo("fruit"),
                ]
            )
            # So we don't choke on these on the cmd line.
            opts.register("", "--pants-workdir")
            opts.register("", "--pants-config-files")

            opts.register("foo", "--bar")
            opts.register("fruit", "--apple")
        self.assertEqual("/qux/baz", opts.for_scope("foo").bar)
        self.assertEqual("/pear/banana", opts.for_scope("fruit").apple)

    def test_bootstrapped_options_ignore_irrelevant_env(self) -> None:
        included = "PANTS_DISTDIR"
        excluded = "NON_PANTS_ENV"
        bootstrapper = OptionsBootstrapper.create(
            env={excluded: "pear", included: "banana"}, args=[], allow_pantsrc=False
        )
        self.assertIn(included, bootstrapper.env)
        self.assertNotIn(excluded, bootstrapper.env)

    def test_create_bootstrapped_multiple_pants_config_files(self) -> None:
        """When given multiple config files, the later files should take precedence when options
        conflict."""

        def create_options_bootstrapper(*config_paths: str) -> OptionsBootstrapper:
            return OptionsBootstrapper.create(
                env={},
                args=[f"--pants-config-files={cp}" for cp in config_paths],
                allow_pantsrc=False,
            )

        def assert_config_read_correctly(
            options_bootstrapper: OptionsBootstrapper,
            *,
            expected_worker_count: int,
        ) -> None:
            options = options_bootstrapper.full_options_for_scopes(
                known_scope_infos=[
                    ScopeInfo(""),
                    ScopeInfo("compile_apt"),
                    ScopeInfo("fruit"),
                ],
            )
            # So we don't choke on these on the cmd line.
            options.register("", "--pants-config-files", type=list)
            options.register("", "--config-override", type=list)
            options.register("compile_apt", "--worker-count")
            options.register("fruit", "--apple")

            self.assertEqual(
                str(expected_worker_count), options.for_scope("compile_apt").worker_count
            )
            self.assertEqual("red", options.for_scope("fruit").apple)

        with temporary_file(binary_mode=False) as fp1, temporary_file(binary_mode=False) as fp2:
            fp1.write(
                dedent(
                    """\
                    [compile_apt]
                    worker_count = 1

                    [fruit]