# 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 os
import sys
import textwrap
import unittest
from io import StringIO

import mozpack.path as mozpath
from mozunit import MockedOpen, main

from mozbuild.configure import ConfigureError, ConfigureSandbox
from mozbuild.configure.options import (
    ConflictingOptionError,
    InvalidOptionError,
    NegativeOptionValue,
    PositiveOptionValue,
)
from mozbuild.util import ReadOnlyNamespace, memoized_property

test_data_path = mozpath.abspath(mozpath.dirname(__file__))
test_data_path = mozpath.join(test_data_path, "data")


class TestConfigure(unittest.TestCase):
    def get_config(
        self, options=[], env={}, configure="moz.configure", prog="/bin/configure"
    ):
        config = {}
        out = StringIO()
        sandbox = ConfigureSandbox(config, env, [prog] + options, out, out)

        sandbox.run(mozpath.join(test_data_path, configure))

        if "--help" in options:
            return out.getvalue(), config
        self.assertEqual("", out.getvalue())
        return config

    def moz_configure(self, source):
        return MockedOpen(
            {os.path.join(test_data_path, "moz.configure"): textwrap.dedent(source)}
        )

    def test_defaults(self):
        config = self.get_config()
        self.maxDiff = None
        self.assertEqual(
            {
                "CHOICES": NegativeOptionValue(),
                "DEFAULTED": PositiveOptionValue(("not-simple",)),
                "IS_GCC": NegativeOptionValue(),
                "OTHER_CHOICES": NegativeOptionValue(),
                "REMAINDER": (
                    PositiveOptionValue(),
                    NegativeOptionValue(),
                    NegativeOptionValue(),
                    NegativeOptionValue(),
                    NegativeOptionValue(),
                    NegativeOptionValue(),
                    NegativeOptionValue(),
                    NegativeOptionValue(),
                    PositiveOptionValue(),
                ),
                "SIMPLE": NegativeOptionValue(),
                "TRIPLET": NegativeOptionValue(),
                "VALUES": NegativeOptionValue(),
                "VALUES2": NegativeOptionValue(),
                "VALUES3": NegativeOptionValue(),
                "WITH_ENV": NegativeOptionValue(),
            },
            config,
        )

    def test_help(self):
        help, config = self.get_config(["--help"], prog="configure")

        self.assertEqual({}, config)
        self.maxDiff = None
        self.assertEqual(
            "Usage: configure [options]\n"
            "\n"
            "Options: [defaults in brackets after descriptions]\n"
            "  Help options:\n"
            "    --help                    Print this message\n"
            "\n"
            "  Options from python/mozbuild/mozbuild/test/configure/data/moz.configure:\n"
            "    --enable-simple           Enable simple\n"
            "    --enable-with-env         Enable with env\n"
            "    --enable-values[=V,...]   Enable values V\n"
            "    --enable-others=V,...     Enable other values V\n"
            "    --enable-triplet=V,V,V    Enable triplet V\n"
            "    --enable-choices[={a,b,c},...]\n"
            "                              Enable choices\n"
            "    --enable-optional-choices[={a,b,c}]\n"
            "                              Enable optional choices\n"
            "    --enable-multiple-choices={a,b,c},...\n"
            "                              Enable multiple choices\n"
            "    --without-thing           Build without thing\n"
            "    --with-stuff              Build with stuff\n"
            "    --option                  Option\n"
            "    --with-returned-default   Returned default [not-simple]\n"
            "    --with-other-default      With other default\n"
            "    --returned-choices        Choices\n"
            "    --enable-foo={x,y}        Enable Foo\n"
            "    --disable-foo             Disable Foo\n"
            "    --enable-include=I        Include I\n"
            "    --with-imports[=I]        Imports I\n"
            "    --indirect-option         Indirectly defined option\n"
            "\n"
            "  Options from python/mozbuild/mozbuild/test/configure/data/included.configure:\n"
            "    --enable-imports-in-template\n"
            "                              Imports in template\n"
            "\n"
            "\n"
            "Environment variables:\n"
            "  Options from python/mozbuild/mozbuild/test/configure/data/moz.configure:\n"
            "    CC                        C Compiler\n"
            "\n",
            help.replace("\\", "/"),
        )

        help, config = self.get_config(
            ["--help", "--enable-simple", "--enable-values=numeric"], prog="configure"
        )

        self.assertEqual({}, config)
        self.maxDiff = None
        self.assertEqual(
            "Usage: configure [options]\n"
            "\n"
            "Options: [defaults in brackets after descriptions]\n"
            "  Help options:\n"
            "    --help                    Print this message\n"
            "\n"
            "  Options from python/mozbuild/mozbuild/test/configure/data/moz.configure:\n"
            "    --enable-simple           Enable simple\n"
            "    --enable-with-env         Enable with env\n"
            "    --enable-values[=V,...]   Enable values V\n"
            "    --enable-others=V,...     Enable other values V\n"
            "    --enable-triplet=V,V,V    Enable triplet V\n"
            "    --enable-choices[={a,b,c},...]\n"
            "                              Enable choices\n"
            "    --enable-optional-choices[={a,b,c}]\n"
            "                              Enable optional choices\n"
            "    --enable-multiple-choices={a,b,c},...\n"
            "                              Enable multiple choices\n"
            "    --without-thing           Build without thing\n"
            "    --with-stuff              Build with stuff\n"
            "    --option                  Option\n"
            "    --with-returned-default   Returned default [simple]\n"
            "    --without-other-default   Without other default\n"
            "    --returned-choices={0,1,2}\n"
            "                              Choices\n"
            "    --enable-foo={x,y}        Enable Foo\n"
            "    --disable-foo             Disable Foo\n"
            "    --enable-include=I        Include I\n"
            "    --with-imports[=I]        Imports I\n"
            "    --indirect-option         Indirectly defined option\n"
            "\n"
            "  Options from python/mozbuild/mozbuild/test/configure/data/included.configure:\n"
            "    --enable-imports-in-template\n"
            "                              Imports in template\n"
            "\n"
            "\n"
            "Environment variables:\n"
            "  Options from python/mozbuild/mozbuild/test/configure/data/moz.configure:\n"
            "    CC                        C Compiler\n"
            "\n",
            help.replace("\\", "/"),
        )

    def test_unknown(self):
        with self.assertRaises(InvalidOptionError):
            self.get_config(["--unknown"])

    def test_simple(self):
        for config in (
            self.get_config(),
            self.get_config(["--disable-simple"]),
            # Last option wins.
            self.get_config(["--enable-simple", "--disable-simple"]),
        ):
            self.assertNotIn("ENABLED_SIMPLE", config)
            self.assertIn("SIMPLE", config)
            self.assertEqual(NegativeOptionValue(), config["SIMPLE"])

        for config in (
            self.get_config(["--enable-simple"]),
            self.get_config(["--disable-simple", "--enable-simple"]),
        ):
            self.assertIn("ENABLED_SIMPLE", config)
            self.assertIn("SIMPLE", config)
            self.assertEqual(PositiveOptionValue(), config["SIMPLE"])
            self.assertIs(config["SIMPLE"], config["ENABLED_SIMPLE"])

        # --enable-simple doesn't take values.
        with self.assertRaises(InvalidOptionError):
            self.get_config(["--enable-simple=value"])

    def test_with_env(self):
        for config in (
            self.get_config(),
            self.get_config(["--disable-with-env"]),
            self.get_config(["--enable-with-env", "--disable-with-env"]),
            self.get_config(env={"MOZ_WITH_ENV": ""}),
            # Options win over environment
            self.get_config(["--disable-with-env"], env={"MOZ_WITH_ENV": "1"}),
        ):
            self.assertIn("WITH_ENV", config)
            self.assertEqual(NegativeOptionValue(), config["WITH_ENV"])

        for config in (
            self.get_config(["--enable-with-env"]),
            self.get_config(["--disable-with-env", "--enable-with-env"]),
            self.get_config(env={"MOZ_WITH_ENV": "1"}),
            self.get_config(["--enable-with-env"], env={"MOZ_WITH_ENV": ""}),
        ):
            self.assertIn("WITH_ENV", config)
            self.assertEqual(PositiveOptionValue(), config["WITH_ENV"])

        with self.assertRaises(InvalidOptionError):
            self.get_config(["--enable-with-env=value"])

        with self.assertRaises(InvalidOptionError):
            self.get_config(env={"MOZ_WITH_ENV": "value"})

    def test_values(self, name="VALUES"):
        for config in (
            self.get_config(),
            self.get_config(["--disable-values"]),
            self.get_config(["--enable-values", "--disable-values"]),
        ):
            self.assertIn(name, config)
            self.assertEqual(NegativeOptionValue(), config[name])

        for config in (
            self.get_config(["--enable-values"]),
            self.get_config(["--disable-values", "--enable-values"]),
        ):
            self.assertIn(name, config)
            self.assertEqual(PositiveOptionValue(), config[name])

        config = self.get_config(["--enable-values=foo"])
        self.assertIn(name, config)
        self.assertEqual(PositiveOptionValue(("foo",)), config[name])

        config = self.get_config(["--enable-values=foo,bar"])
        self.assertIn(name, config)
        self.assertTrue(config[name])
        self.assertEqual(PositiveOptionValue(("foo", "bar")), config[name])

    def test_values2(self):
        self.test_values("VALUES2")

    def test_values3(self):
        self.test_values("VALUES3")

    def test_returned_default(self):
        config = self.get_config(["--enable-simple"])
        self.assertIn("DEFAULTED", config)
        self.assertEqual(PositiveOptionValue(("simple",)), config["DEFAULTED"])

        config = self.get_config(["--disable-simple"])
        self.assertIn("DEFAULTED", config)
        self.assertEqual(PositiveOptionValue(("not-simple",)), config["DEFAULTED"])

    def test_returned_choices(self):
        for val in ("a", "b", "c"):
            config = self.get_config(
                ["--enable-values=alpha", "--returned-choices=%s" % val]
            )
            self.assertIn("CHOICES", config)
            self.assertEqual(PositiveOptionValue((val,)), config["CHOICES"])

        for val in ("0", "1", "2"):
            config = self.get_config(
                ["--enable-values=numeric", "--returned-choices=%s" % val]
            )
            self.assertIn("CHOICES", config)
            self.assertEqual(PositiveOptionValue((val,)), config["CHOICES"])

        with self.assertRaises(InvalidOptionError):
            self.get_config(["--enable-values=numeric", "--returned-choices=a"])

        with self.assertRaises(InvalidOptionError):
            self.get_config(["--enable-values=alpha", "--returned-choices=0"])

    def test_included(self):
        config = self.get_config(env={"CC": "gcc"})
        self.assertIn("IS_GCC", config)
        self.assertEqual(config["IS_GCC"], True)

        config = self.get_config(["--enable-include=extra.configure", "--extra"])
        self.assertIn("EXTRA", config)
        self.assertEqual(PositiveOptionValue(), config["EXTRA"])

        with self.assertRaises(InvalidOptionError):
            self.get_config(["--extra"])

    def test_template(self):
        config = self.get_config(env={"CC": "gcc"})
        self.assertIn("CFLAGS", config)
        self.assertEqual(config["CFLAGS"], ["-Werror=foobar"])

        config = self.get_config(env={"CC": "clang"})
        self.assertNotIn("CFLAGS", config)

    def test_imports(self):
        config = {}
        out = StringIO()
        sandbox = ConfigureSandbox(config, {}, ["configure"], out, out)

        with self.assertRaises(ImportError):
            exec(
                textwrap.dedent(
                    """
                @template
                def foo():
                    import sys
                foo()"""
                ),
                sandbox,
            )

        exec(
            textwrap.dedent(
                """
            @template
            @imports('sys')
            def foo():
                return sys"""
            ),
            sandbox,
        )

        self.assertIs(sandbox["foo"](), sys)

        # os.path after an import is a mix of vanilla os.path and sandbox os.path.
        os_path = {}
        exec("from os.path import *", {}, os_path)
        os_path.update(sandbox.OS.path.__dict__)
        os_path = ReadOnlyNamespace(**os_path)

        exec(
            textwrap.dedent(
                """
            @template
            @imports(_from='os', _import='path')
            def foo():
                return path"""
            ),
            sandbox,
        )

        self.assertEqual(sandbox["foo"](), os_path)

        exec(
            textwrap.dedent(
                """
            @template
            @imports(_from='os', _import='path', _as='os_path')
            def foo():
                return os_path"""
            ),
            sandbox,
        )

        self.assertEqual(sandbox["foo"](), os_path)

        exec(
            textwrap.dedent(
                """
            @template
            @imports('__builtin__')
            def foo():
                return __builtin__"""
            ),
            sandbox,
        )

        with self.assertRaises(Exception) as e:
            sandbox["foo"]()
        self.assertEqual(str(e.exception), "Importing __builtin__ is forbidden")

        exec(
            textwrap.dedent(
                """
            @template
            @imports(_from='__builtin__', _import='open')
            def foo():
                return open('%s')"""
                % os.devnull
            ),
            sandbox,
        )

        f = sandbox["foo"]()
        self.assertEqual(f.name, os.devnull)
        f.close()

        # This used to unlock the sandbox
        exec(
            textwrap.dedent(
                """
            @template
            @imports(_import='__builtin__', _as='__builtins__')
            def foo():
                import sys
                return sys"""
            ),
            sandbox,
        )

        with self.assertRaises(Exception) as e:
            sandbox["foo"]()
        self.assertEqual(str(e.exception), "Importing __builtin__ is forbidden")

        exec(
            textwrap.dedent(
                """
            @template
            @imports('__sandbox__')
            def foo():
                return __sandbox__"""
            ),
            sandbox,
        )

        self.assertIs(sandbox["foo"](), sandbox)

        exec(
            textwrap.dedent(
                """
            @template
            @imports(_import='__sandbox__', _as='s')
            def foo():
                return s"""
            ),
            sandbox,
        )

        self.assertIs(sandbox["foo"](), sandbox)

        # Nothing leaked from the function being executed
        self.assertEqual(list(sandbox), ["__builtins__", "foo"])
        self.assertEqual(sandbox["__builtins__"], ConfigureSandbox.BUILTINS)

        exec(
            textwrap.dedent(
                """
            @template
            @imports('sys')
            def foo():
                @depends(when=True)
                def bar():
                    return sys
                return bar
            bar = foo()"""
            ),
            sandbox,
        )

        with self.assertRaises(NameError) as e:
            sandbox._depends[sandbox["bar"]].result()

        self.assertIn("name 'sys' is not defined", str(e.exception))

    def test_apply_imports(self):
        imports = []

        class CountApplyImportsSandbox(ConfigureSandbox):
            def _apply_imports(self, *args, **kwargs):
                imports.append((args, kwargs))
                super(CountApplyImportsSandbox, self)._apply_imports(*args, **kwargs)

        config = {}
        out = StringIO()
        sandbox = CountApplyImportsSandbox(config, {}, ["configure"], out, out)

        exec(
            textwrap.dedent(
                """
            @template
            @imports('sys')
            def foo():
                return sys
            foo()
            foo()"""
            ),
            sandbox,
        )

        self.assertEqual(len(imports), 1)

    def test_import_wrapping(self):
        bar = object()
        foo = ReadOnlyNamespace(bar=bar)

        class BasicWrappingSandbox(ConfigureSandbox):
            @memoized_property
            def _wrapped_foo(self):
                return foo

        config = {}
        out = StringIO()
        sandbox = BasicWrappingSandbox(config, {}, ["configure"], out, out)

        exec(
            textwrap.dedent(
                """
            @template
            @imports('foo')
            def toplevel():
                return foo
            @template
            @imports('foo.bar')
            def bar():
                return foo.bar
            @template
            @imports('foo.bar')
            def bar_upper():
                return foo
            @template
            @imports(_from='foo', _import='bar')
            def from_import():
                return bar
            @template
            @imports(_from='foo', _import='bar', _as='custom_name')
            def from_import_as():
                return custom_name
            @template
            @imports(_import='foo', _as='custom_name')
            def import_as():
                return custom_name
            """
            ),
            sandbox,
        )
        self.assertIs(sandbox["toplevel"](), foo)
        self.assertIs(sandbox["bar"](), bar)
        self.assertIs(sandbox["bar_upper"](), foo)
        self.assertIs(sandbox["from_import"](), bar)
        self.assertIs(sandbox["from_import_as"](), bar)
        self.assertIs(sandbox["import_as"](), foo)

    def test_os_path(self):
        config = self.get_config(["--with-imports=%s" % __file__])
        self.assertIn("HAS_ABSPATH", config)
        self.assertEqual(config["HAS_ABSPATH"], True)
        self.assertIn("HAS_GETATIME", config)
        self.assertEqual(config["HAS_GETATIME"], True)
        self.assertIn("HAS_GETATIME2", config)
        self.assertEqual(config["HAS_GETATIME2"], False)

    def test_template_call(self):
        config = self.get_config(env={"CC": "gcc"})
        self.assertIn("TEMPLATE_VALUE", config)
        self.assertEqual(config["TEMPLATE_VALUE"], 42)
        self.assertIn("TEMPLATE_VALUE_2", config)
        self.assertEqual(config["TEMPLATE_VALUE_2"], 21)

    def test_template_imports(self):
        config = self.get_config(["--enable-imports-in-template"])
        self.assertIn("PLATFORM", config)
        self.assertEqual(config["PLATFORM"], sys.platform)

    def test_decorators(self):
        config = {}
        out = StringIO()
        sandbox = ConfigureSandbox(config, {}, ["configure"], out, out)

        sandbox.include_file(mozpath.join(test_data_path, "decorators.configure"))

        self.assertNotIn("FOO", sandbox)
        self.assertNotIn("BAR", sandbox)
        self.assertNotIn("QUX", sandbox)

    def test_set_config(self):
        def get_config(*args):
            return self.get_config(*args, configure="set_config.configure")

        help, config = get_config(["--help"])
        self.assertEqual(config, {})

        config = get_config(["--set-foo"])
        self.assertIn("FOO", config)
        self.assertEqual(config["FOO"], True)

        config = get_config(["--set-bar"])
        self.assertNotIn("FOO", config)
        self.assertIn("BAR", config)
        self.assertEqual(config["BAR"], True)

        config = get_config(["--set-value=qux"])
        self.assertIn("VALUE", config)
        self.assertEqual(config["VALUE"], "qux")

        config = get_config(["--set-name=hoge"])
        self.assertIn("hoge", config)
        self.assertEqual(config["hoge"], True)

        config = get_config([])
        self.assertEqual(config, {"BAR": False})

        with self.assertRaises(ConfigureError):
            # Both --set-foo and --set-name=FOO are going to try to
            # set_config('FOO'...)
            get_config(["--set-foo", "--set-name=FOO"])

    def test_set_config_when(self):
        with self.moz_configure(
            """
            option('--with-qux', help='Qux')
            set_config('FOO', 'foo', when=True)
            set_config('BAR', 'bar', when=False)
            set_config('QUX', 'qux', when='--with-qux')
        """
        ):
            config = self.get_config()
            self.assertEqual(
                config,
                {
                    "FOO": "foo",
                },
            )
            config = self.get_config(["--with-qux"])
            self.assertEqual(
                config,
                {
                    "FOO": "foo",
                    "QUX": "qux",
                },
            )

    def test_set_config_when_disable(self):
        with self.moz_configure(
            """
            option('--disable-baz', help='Disable baz')
            set_config('BAZ', True, when='--enable-baz')
        """
        ):
            config = self.get_config()
            self.assertEqual(config["BAZ"], True)
            config = self.get_config(["--enable-baz"])
            self.assertEqual(config["BAZ"], True)
            config = self.get_config(["--disable-baz"])
            self.assertEqual(config, {})

    def test_set_define(self):
        def get_config(*args):
            return self.get_config(*args, configure="set_define.configure")

        help, config = get_config(["--help"])
        self.assertEqual(config, {"DEFINES": {}})

        config = get_config(["--set-foo"])
        self.assertIn("FOO", config["DEFINES"])
        self.assertEqual(config["DEFINES"]["FOO"], True)

        config = get_config(["--set-bar"])
        self.assertNotIn("FOO", config["DEFINES"])
        self.assertIn("BAR", config["DEFINES"])
        self.assertEqual(config["DEFINES"]["BAR"], True)

        config = get_config(["--set-value=qux"])
        self.assertIn("VALUE", config["DEFINES"])
        self.assertEqual(config["DEFINES"]["VALUE"], "qux")

        config = get_config(["--set-name=hoge"])
        self.assertIn("hoge", config["DEFINES"])
        self.assertEqual(config["DEFINES"]["hoge"], True)

        config = get_config([])
        self.assertEqual(config["DEFINES"], {"BAR": False})

        with self.assertRaises(ConfigureError):
            # Both --set-foo and --set-name=FOO are going to try to
            # set_define('FOO'...)
            get_config(["--set-foo", "--set-name=FOO"])

    def test_set_define_when(self):
        with self.moz_configure(
            """
            option('--with-qux', help='Qux')
            set_define('FOO', 'foo', when=True)
            set_define('BAR', 'bar', when=False)
            set_define('QUX', 'qux', when='--with-qux')
        """
        ):
            config = self.get_config()
            self.assertEqual(
                config["DEFINES"],
                {
                    "FOO": "foo",
                },
            )
            config = self.get_config(["--with-qux"])
            self.assertEqual(
                config["DEFINES"],
                {
                    "FOO": "foo",
                    "QUX": "qux",
                },
            )

    def test_set_define_when_disable(self):
        with self.moz_configure(
            """
            option('--disable-baz', help='Disable baz')
            set_define('BAZ', True, when='--enable-baz')
        """
        ):
            config = self.get_config()
            self.assertEqual(config["DEFINES"]["BAZ"], True)
            config = self.get_config(["--enable-baz"])
            self.assertEqual(config["DEFINES"]["BAZ"], True)
            config = self.get_config(["--disable-baz"])
            self.assertEqual(config["DEFINES"], {})

    def test_imply_option_simple(self):
        def get_config(*args):
            return self.get_config(*args, configure="imply_option/simple.configure")

        help, config = get_config(["--help"])
        self.assertEqual(config, {})

        config = get_config([])
        self.assertEqual(config, {})

        config = get_config(["--enable-foo"])
        self.assertIn("BAR", config)
        self.assertEqual(config["BAR"], PositiveOptionValue())

        with self.assertRaises(InvalidOptionError) as e:
            get_config(["--enable-foo", "--disable-bar"])

        self.assertEqual(
            str(e.exception),
            "'--enable-bar' implied by '--enable-foo' conflicts with "
            "'--disable-bar' from the command-line",
        )

    def test_imply_option_negative(self):
        def get_config(*args):
            return self.get_config(*args, configure="imply_option/negative.configure")

        help, config = get_config(["--help"])
        self.assertEqual(config, {})

        config = get_config([])
        self.assertEqual(config, {})

        config = get_config(["--enable-foo"])
        self.assertIn("BAR", config)
        self.assertEqual(config["BAR"], NegativeOptionValue())

        with self.assertRaises(InvalidOptionError) as e:
            get_config(["--enable-foo", "--enable-bar"])

        self.assertEqual(
            str(e.exception),
            "'--disable-bar' implied by '--enable-foo' conflicts with "
            "'--enable-bar' from the command-line",
        )

        config = get_config(["--disable-hoge"])
        self.assertIn("BAR", config)
        self.assertEqual(config["BAR"], NegativeOptionValue())

        with self.assertRaises(InvalidOptionError) as e:
            get_config(["--disable-hoge", "--enable-bar"])

        self.assertEqual(
            str(e.exception),
            "'--disable-bar' implied by '--disable-hoge' conflicts with "
            "'--enable-bar' from the command-line",
        )

    def test_imply_option_values(self):
        def get_config(*args):
            return self.get_config(*args, configure="imply_option/values.configure")

        help, config = get_config(["--help"])
        self.assertEqual(config, {})

        config = get_config([])
        self.assertEqual(config, {})

        config = get_config(["--enable-foo=a"])
        self.assertIn("BAR", config)
        self.assertEqual(config["BAR"], PositiveOptionValue(("a",)))

        config = get_config(["--enable-foo=a,b"])
        self.assertIn("BAR", config)
        self.assertEqual(config["BAR"], PositiveOptionValue(("a", "b")))

        with self.assertRaises(InvalidOptionError) as e:
            get_config(["--enable-foo=a,b", "--disable-bar"])

        self.assertEqual(
            str(e.exception),
            "'--enable-bar=a,b' implied by '--enable-foo' conflicts with "
            "'--disable-bar' from the command-line",
        )

    def test_imply_option_infer(self):
        def get_config(*args):
            return self.get_config(*args, configure="imply_option/infer.configure")

        help, config = get_config(["--help"])
        self.assertEqual(config, {})

        config = get_config([])
        self.assertEqual(config, {})

        with self.assertRaises(InvalidOptionError) as e:
            get_config(["--enable-foo", "--disable-bar"])

        self.assertEqual(
            str(e.exception),
            "'--enable-bar' implied by '--enable-foo' conflicts with "
            "'--disable-bar' from the command-line",
        )

        with self.assertRaises(ConfigureError) as e:
            self.get_config([], configure="imply_option/infer_ko.configure")

        self.assertEqual(
            str(e.exception),
            "Cannot infer what implies '--enable-bar'. Please add a `reason` "
            "to the `imply_option` call.",
        )

    def test_imply_option_immediate_value(self):
        def get_config(*args):
            return self.get_config(*args, configure="imply_option/imm.configure")

        help, config = get_config(["--help"])
        self.assertEqual(config, {})

        config = get_config([])
        self.assertEqual(config, {})

        config_path = mozpath.abspath(
            mozpath.join(test_data_path, "imply_option", "imm.configure")
        )

        with self.assertRaisesRegex(
            InvalidOptionError,
            "--enable-foo' implied by 'imply_option at %s:7' conflicts "
            "with '--disable-foo' from the command-line" % config_path,
        ):
            get_config(["--disable-foo"])

        with self.assertRaisesRegex(
            InvalidOptionError,
            "--enable-bar=foo,bar' implied by 'imply_option at %s:18' "
            "conflicts with '--enable-bar=a,b,c' from the command-line" % config_path,
        ):
            get_config(["--enable-bar=a,b,c"])

        with self.assertRaisesRegex(
            InvalidOptionError,
            "--enable-baz=BAZ' implied by 'imply_option at %s:29' "
            "conflicts with '--enable-baz=QUUX' from the command-line" % config_path,
        ):
            get_config(["--enable-baz=QUUX"])

    def test_imply_option_failures(self):
        with self.assertRaises(ConfigureError) as e:
            with self.moz_configure(
                """
                imply_option('--with-foo', ('a',), 'bar')
            """
            ):
                self.get_config()

        self.assertEqual(
            str(e.exception),
            "`--with-foo`, emitted from `%s` line 2, is unknown."
            % mozpath.join(test_data_path, "moz.configure"),
        )

        with self.assertRaises(TypeError) as e:
            with self.moz_configure(
                """
                imply_option('--with-foo', 42, 'bar')

                option('--with-foo', help='Foo')
                @depends('--with-foo')
                def foo(value):
                    return value
            """
            ):
                self.get_config()

        self.assertEqual(str(e.exception), "Unexpected type: 'int'")

    def test_imply_option_when(self):
        with self.moz_configure(
            """
            option('--with-foo', help='Foo')
            imply_option('--with-qux', True, when='--with-foo')
            option('--with-qux', help='Qux')
            set_config('QUX', depends('--with-qux')(lambda x: x))
        """
        ):
            config = self.get_config()
            self.assertEqual(
                config,
                {
                    "QUX": NegativeOptionValue(),
                },
            )

            config = self.get_config(["--with-foo"])
            self.assertEqual(
                config,
                {
                    "QUX": PositiveOptionValue(),
                },
            )

    def test_imply_option_dependency_loop(self):
        with self.moz_configure(
            """
            option('--without-foo', help='Foo')

            @depends('--with-foo')
            def qux_default(foo):
                return bool(foo)

            option('--with-qux', default=qux_default, help='Qux')

            imply_option('--with-foo', depends('--with-qux')(lambda x: x or None))

            set_config('FOO', depends('--with-foo')(lambda x: x))
            set_config('QUX', depends('--with-qux')(lambda x: x))
        """
        ):
            config = self.get_config()
            self.assertEqual(
                config,
                {
                    "FOO": PositiveOptionValue(),
                    "QUX": PositiveOptionValue(),
                },
            )

            config = self.get_config(["--without-foo"])
            self.assertEqual(
                config,
                {
                    "FOO": NegativeOptionValue(),
                    "QUX": NegativeOptionValue(),
                },
            )

            config = self.get_config(["--with-qux"])
            self.assertEqual(
                config,
                {
                    "FOO": PositiveOptionValue(),
                    "QUX": PositiveOptionValue(),
                },
            )

            with self.assertRaises(InvalidOptionError) as e:
                config = self.get_config(["--without-foo", "--with-qux"])

            self.assertEqual(
                str(e.exception),
                "'--with-foo' implied by '--with-qux' conflicts "
                "with '--without-foo' from the command-line",
            )

            config = self.get_config(["--without-qux"])
            self.assertEqual(
                config,
                {
                    "FOO": PositiveOptionValue(),
                    "QUX": NegativeOptionValue(),
                },
            )

        with self.moz_configure(
            """
            option('--with-foo', help='Foo')

            @depends('--with-foo')
            def qux_default(foo):
                return bool(foo)

            option('--with-qux', default=qux_default, help='Qux')

            imply_option('--with-foo', depends('--with-qux')(lambda x: x or None))

            set_config('FOO', depends('--with-foo')(lambda x: x))
            set_config('QUX', depends('--with-qux')(lambda x: x))
        """
        ):
            config = self.get_config()
            self.assertEqual(
                config,
                {
                    "FOO": NegativeOptionValue(),
                    "QUX": NegativeOptionValue(),
                },
            )

            config = self.get_config(["--with-foo"])
            self.assertEqual(
                config,
                {
                    "FOO": PositiveOptionValue(),
                    "QUX": PositiveOptionValue(),
                },
            )

            with self.assertRaises(InvalidOptionError) as e:
                config = self.get_config(["--with-qux"])

            self.assertEqual(
                str(e.exception),
                "'--with-foo' implied by '--with-qux' conflicts "
                "with '--without-foo' from the default",
            )

            with self.assertRaises(InvalidOptionError) as e:
                config = self.get_config(["--without-foo", "--with-qux"])

            self.assertEqual(
                str(e.exception),
                "'--with-foo' implied by '--with-qux' conflicts "
                "with '--without-foo' from the command-line",
            )

            config = self.get_config(["--without-qux"])
            self.assertEqual(
                config,
                {
                    "FOO": NegativeOptionValue(),
                    "QUX": NegativeOptionValue(),
                },
            )

        config_path = mozpath.abspath(mozpath.join(test_data_path, "moz.configure"))

        # Same test as above, but using `when` in the `imply_option`.
        with self.moz_configure(
            """
            option('--with-foo', help='Foo')

            @depends('--with-foo')
            def qux_default(foo):
                return bool(foo)

            option('--with-qux', default=qux_default, help='Qux')

            imply_option('--with-foo', True, when='--with-qux')

            set_config('FOO', depends('--with-foo')(lambda x: x))
            set_config('QUX', depends('--with-qux')(lambda x: x))
        """
        ):
            config = self.get_config()
            self.assertEqual(
                config,
                {
                    "FOO": NegativeOptionValue(),
                    "QUX": NegativeOptionValue(),
                },
            )

            config = self.get_config(["--with-foo"])
            self.assertEqual(
                config,
                {
                    "FOO": PositiveOptionValue(),
                    "QUX": PositiveOptionValue(),
                },
            )

            with self.assertRaises(InvalidOptionError) as e:
                config = self.get_config(["--with-qux"])

            self.assertEqual(
                str(e.exception),
                "'--with-foo' implied by 'imply_option at %s:10' conflicts "
                "with '--without-foo' from the default" % config_path,
            )

            with self.assertRaises(InvalidOptionError) as e:
                config = self.get_config(["--without-foo", "--with-qux"])

            self.assertEqual(
                str(e.exception),
                "'--with-foo' implied by 'imply_option at %s:10' conflicts "
                "with '--without-foo' from the command-line" % config_path,
            )

            config = self.get_config(["--without-qux"])
            self.assertEqual(
                config,
                {
                    "FOO": NegativeOptionValue(),
                    "QUX": NegativeOptionValue(),
                },
            )

    def test_imply_option_recursion(self):
        config_path = mozpath.abspath(mozpath.join(test_data_path, "moz.configure"))

        message = (
            "'--without-foo' appears somewhere in the direct or indirect dependencies "
            "when resolving imply_option at %s:8" % config_path
        )

        with self.moz_configure(
            """
            option('--without-foo', help='Foo')

            imply_option('--with-qux', depends('--with-foo')(lambda x: x or None))

            option('--with-qux', help='Qux')

            imply_option('--with-foo', depends('--with-qux')(lambda x: x or None))

            set_config('FOO', depends('--with-foo')(lambda x: x))
            set_config('QUX', depends('--with-qux')(lambda x: x))
        """
        ):
            # Note: no error is detected when the depends function in the
            # imply_options resolve to None, which disables the imply_option.

            with self.assertRaises(ConfigureError) as e:
                self.get_config()

            self.assertEqual(str(e.exception), message)

            with self.assertRaises(ConfigureError) as e:
                self.get_config(["--with-qux"])

            self.assertEqual(str(e.exception), message)

            with self.assertRaises(ConfigureError) as e:
                self.get_config(["--without-foo", "--with-qux"])

            self.assertEqual(str(e.exception), message)

    def test_imply_option_conflict(self):
        moz_configure = """
            option('--with-foo', help='Foo')
            option('--with-env-foo', help='Foo')
            imply_option('--with-qux', True)
            imply_option('QUX', "FOO", when='--with-env-foo')
            imply_option('--with-qux', "FOO", when='--with-foo')
            option('--with-qux', env="QUX", nargs='*', help='Qux')
            set_config('QUX', depends('--with-qux')(lambda x: x))
        """
        with self.assertRaises(ConflictingOptionError) as e:
            with self.moz_configure(moz_configure):
                self.get_config(["--with-foo"])

        self.assertEqual(
            str(e.exception),
            "Cannot add '--with-qux=FOO' to the implied set because it conflicts"
            " with '--with-qux' that was added earlier",
        )

        with self.assertRaises(InvalidOptionError) as e:
            with self.moz_configure(moz_configure):
                self.get_config(["--with-env-foo"])

        config_path = mozpath.abspath(mozpath.join(test_data_path, "moz.configure"))

        # TODO: the error message is weird.
        self.assertEqual(
            str(e.exception),
            "'QUX=FOO' implied by 'imply_option at %s:5' conflicts "
            "with '--with-qux' from the implied" % config_path,
        )

        with self.assertRaises(InvalidOptionError) as e:
            with self.moz_configure(moz_configure):
                self.get_config(["--with-foo", "--with-env-foo"])

        self.assertEqual(
            str(e.exception),
            "Cannot add '--with-qux=FOO' to the implied set because it conflicts"
            " with '--with-qux' that was added earlier",
        )

    def test_option_failures(self):
        with self.assertRaises(ConfigureError) as e:
            with self.moz_configure('option("--with-foo", help="Foo")'):
                self.get_config()

        self.assertEqual(
            str(e.exception),
            "Option `--with-foo` is not handled ; reference it with a @depends",
        )

        with self.assertRaises(ConfigureError) as e:
            with self.moz_configure(
                """
                option("--with-foo", help="Foo")
                option("--with-foo", help="Foo")
            """
            ):
                self.get_config()

        self.assertEqual(str(e.exception), "Option `--with-foo` already defined")

        with self.assertRaises(ConfigureError) as e:
            with self.moz_configure(
                """
                option(env="MOZ_FOO", help="Foo")
                option(env="MOZ_FOO", help="Foo")
            """
            ):
                self.get_config()

        self.assertEqual(str(e.exception), "Option `MOZ_FOO` already defined")

        with self.assertRaises(ConfigureError) as e:
            with self.moz_configure(
                """
                option('--with-foo', env="MOZ_FOO", help="Foo")
                option(env="MOZ_FOO", help="Foo")
            """
            ):
                self.get_config()

        self.assertEqual(str(e.exception), "Option `MOZ_FOO` already defined")

        with self.assertRaises(ConfigureError) as e:
            with self.moz_configure(
                """
                option(env="MOZ_FOO", help="Foo")
                option('--with-foo', env="MOZ_FOO", help="Foo")
            """
            ):
                self.get_config()

        self.assertEqual(str(e.exception), "Option `MOZ_FOO` already defined")

        with self.assertRaises(ConfigureError) as e:
            with self.moz_configure(
                """
                option('--with-foo', env="MOZ_FOO", help="Foo")
                option('--with-foo', help="Foo")
            """
            ):
                self.get_config()

        self.assertEqual(str(e.exception), "Option `--with-foo` already defined")

    def test_option_when(self):
        with self.moz_configure(
            """
            option('--with-foo', help='Foo', when=True)
            option('--with-bar', help='Bar', when=False)
            option('--with-qux', env="QUX", help='Qux', when='--with-foo')

            set_config('FOO', depends('--with-foo', when=True)(lambda x: x))
            set_config('BAR', depends('--with-bar', when=False)(lambda x: x))
            set_config('QUX', depends('--with-qux', when='--with-foo')(lambda x: x))
        """
        ):
            config = self.get_config()
            self.assertEqual(
                config,
                {
                    "FOO": NegativeOptionValue(),
                },
            )

            config = self.get_config(["--with-foo"])
            self.assertEqual(
                config,
                {
                    "FOO": PositiveOptionValue(),
                    "QUX": NegativeOptionValue(),
                },
            )

            config = self.get_config(["--with-foo", "--with-qux"])
            self.assertEqual(
                config,
                {
                    "FOO": PositiveOptionValue(),
                    "QUX": PositiveOptionValue(),
                },
            )

            with self.assertRaises(InvalidOptionError) as e:
                self.get_config(["--with-bar"])

            self.assertEqual(
                str(e.exception), "--with-bar is not available in this configuration"
            )

            with self.assertRaises(InvalidOptionError) as e:
                self.get_config(["--with-qux"])

            self.assertEqual(
                str(e.exception), "--with-qux is not available in this configuration"
            )

            with self.assertRaises(InvalidOptionError) as e:
                self.get_config(["QUX=1"])

            self.assertEqual(
                str(e.exception), "QUX is not available in this configuration"
            )

            config = self.get_config(env={"QUX": "1"})
            self.assertEqual(
                config,
                {
                    "FOO": NegativeOptionValue(),
                },
            )

            help, config = self.get_config(["--help"])
            self.assertEqual(
                help.replace("\\", "/"),
                textwrap.dedent(
                    """\
                Usage: configure [options]

                Options: [defaults in brackets after descriptions]
                  Help options:
                    --help                    Print this message

                  Options from python/mozbuild/mozbuild/test/configure/data/moz.configure:
                    --with-foo                Foo

            """
                ),
            )

            help, config = self.get_config(["--help", "--with-foo"])
            self.assertEqual(
                help.replace("\\", "/"),
                textwrap.dedent(
                    """\
                Usage: configure [options]

                Options: [defaults in brackets after descriptions]
                  Help options:
                    --help                    Print this message

                  Options from python/mozbuild/mozbuild/test/configure/data/moz.configure:
                    --with-foo                Foo
                    --with-qux                Qux

            """
                ),
            )

        with self.moz_configure(
            """
            option('--with-foo', help='Foo', when=True)
            set_config('FOO', depends('--with-foo')(lambda x: x))
        """
        ):
            with self.assertRaises(ConfigureError) as e:
                self.get_config()

            self.assertEqual(
                str(e.exception),
                "@depends function needs the same `when` as " "options it depends on",
            )

        with self.moz_configure(
            """
            @depends(when=True)
            def always():
                return True
            @depends(when=True)
            def always2():
                return True
            option('--with-foo', help='Foo', when=always)
            set_config('FOO', depends('--with-foo', when=always2)(lambda x: x))
        """
        ):
            with self.assertRaises(ConfigureError) as e:
                self.get_config()

            self.assertEqual(
                str(e.exception),
                "@depends function needs the same `when` as " "options it depends on",
            )

        with self.moz_configure(
            """
            @depends(when=True)
            def always():
                return True
            @depends(when=True)
            def always2():
                return True
            with only_when(always2):
                option('--with-foo', help='Foo', when=always)
                # include() triggers resolution of its dependencies, and their
                # side effects.
                include(depends('--with-foo', when=always)(lambda x: x))
                # The sandbox should figure that the `when` here is
                # appropriate. Bad behavior in CombinedDependsFunction.__eq__
                # made this fail in the past.
                set_config('FOO', depends('--with-foo', when=always)(lambda x: x))
        """
        ):
            self.get_config()

        with self.moz_configure(
            """
            option('--with-foo', help='Foo')
            option('--without-bar', help='Bar', when='--with-foo')
            option('--with-qux', help='Qux', when='--with-bar')
            set_config('QUX', True, when='--with-qux')
        """
        ):
            # These are valid:
            self.get_config(["--with-foo"])
            self.get_config(["--with-foo", "--with-bar"])
            self.get_config(["--with-foo", "--without-bar"])
            self.get_config(["--with-foo", "--with-bar", "--with-qux"])
            self.get_config(["--with-foo", "--with-bar", "--without-qux"])
            with self.assertRaises(InvalidOptionError) as e:
                self.get_config(["--with-bar"])
            with self.assertRaises(InvalidOptionError) as e:
                self.get_config(["--without-bar"])
            with self.assertRaises(InvalidOptionError) as e:
                self.get_config(["--with-qux"])
            with self.assertRaises(InvalidOptionError) as e:
                self.get_config(["--without-qux"])
            with self.assertRaises(InvalidOptionError) as e:
                self.get_config(["--with-foo", "--without-bar", "--with-qux"])
            with self.assertRaises(InvalidOptionError) as e:
                self.get_config(["--with-foo", "--without-bar", "--without-qux"])

    def test_include_failures(self):
        with self.assertRaises(ConfigureError) as e:
            with self.moz_configure('include("../foo.configure")'):
                self.get_config()

        self.assertEqual(
            str(e.exception),
            "Cannot include `%s` because it is not in a subdirectory of `%s`"
            % (
                mozpath.normpath(mozpath.join(test_data_path, "..", "foo.configure")),
                mozpath.normsep(test_data_path),
            ),
        )

        with self.assertRaises(ConfigureError) as e:
            with self.moz_configure(
                """
                include('extra.configure')
                include('extra.configure')
            """
            ):
                self.get_config()

        self.assertEqual(
            str(e.exception),
            "Cannot include `%s` because it was included already."
            % mozpath.normpath(mozpath.join(test_data_path, "extra.configure")),
        )

        with self.assertRaises(TypeError) as e:
            with self.moz_configure(
                """
                include(42)
            """
            ):
                self.get_config()

        self.assertEqual(str(e.exception), "Unexpected type: 'int'")

    def test_include_when(self):
        with MockedOpen(
            {
                os.path.join(test_data_path, "moz.configure"): textwrap.dedent(
                    """
                option('--with-foo', help='Foo')

                include('always.configure', when=True)
                include('never.configure', when=False)
                include('foo.configure', when='--with-foo')

                set_config('FOO', foo)
                set_config('BAR', bar)
                set_config('QUX', qux)
            """
                ),
                os.path.join(test_data_path, "always.configure"): textwrap.dedent(
                    """
                option('--with-bar', help='Bar')
                @depends('--with-bar')
                def bar(x):
                    if x:
                        return 'bar'
            """
                ),
                os.path.join(test_data_path, "never.configure"): textwrap.dedent(
                    """
                option('--with-qux', help='Qux')
                @depends('--with-qux')
                def qux(x):
                    if x:
                        return 'qux'
            """
                ),
                os.path.join(test_data_path, "foo.configure"): textwrap.dedent(
                    """
                option('--with-foo-really', help='Really foo')
                @depends('--with-foo-really')
                def foo(x):
                    if x:
                        return 'foo'

                include('foo2.configure', when='--with-foo-really')
            """
                ),
                os.path.join(test_data_path, "foo2.configure"): textwrap.dedent(
                    """
                set_config('FOO2', True)
            """
                ),
            }
        ):
            config = self.get_config()
            self.assertEqual(config, {})

            config = self.get_config(["--with-foo"])
            self.assertEqual(config, {})

            config = self.get_config(["--with-bar"])
            self.assertEqual(
                config,
                {
                    "BAR": "bar",
                },
            )

            with self.assertRaises(InvalidOptionError) as e:
                self.get_config(["--with-qux"])

            self.assertEqual(
                str(e.exception), "--with-qux is not available in this configuration"
            )

            config = self.get_config(["--with-foo", "--with-foo-really"])
            self.assertEqual(
                config,
                {
                    "FOO": "foo",
                    "FOO2": True,
                },
            )

    def test_sandbox_failures(self):
        with self.assertRaises(KeyError) as e:
            with self.moz_configure(
                """
                include = 42
            """
            ):
                self.get_config()

        self.assertIn("Cannot reassign builtins", str(e.exception))

        with self.assertRaises(KeyError) as e:
            with self.moz_configure(
                """
                foo = 42
            """
            ):
                self.get_config()

        self.assertIn(
            "Cannot assign `foo` because it is neither a @depends nor a " "@template",
            str(e.exception),
        )

    def test_depends_failures(self):
        with self.assertRaises(ConfigureError) as e:
            with self.moz_configure(
                """
                @depends()
                def foo():
                    return
            """
            ):
                self.get_config()

        self.assertEqual(str(e.exception), "@depends needs at least one argument")

        with self.assertRaises(ConfigureError) as e:
            with self.moz_configure(
                """
                @depends('--with-foo')
                def foo(value):
                    return value
            """
            ):
                self.get_config()

        self.assertEqual(
            str(e.exception),
            "'--with-foo' is not a known option. Maybe it's " "declared too late?",
        )

        with self.assertRaises(ConfigureError) as e:
            with self.moz_configure(
                """
                @depends('--with-foo=42')
                def foo(value):
                    return value
            """
            ):
                self.get_config()

        self.assertEqual(str(e.exception), "Option must not contain an '='")

        with self.assertRaises(TypeError) as e:
            with self.moz_configure(
                """
                @depends(42)
                def foo(value):
                    return value
            """
            ):
                self.get_config()

        self.assertEqual(
            str(e.exception),
            "Cannot use object of type 'int' as argument " "to @depends",
        )

        with self.assertRaises(ConfigureError) as e:
            with self.moz_configure(
                """
                @depends('--help')
                def foo(value):
                    yield
            """
            ):
                self.get_config()

        self.assertEqual(
            str(e.exception), "Cannot decorate generator functions with @depends"
        )

        with self.assertRaises(TypeError) as e:
            with self.moz_configure(
                """
                @depends('--help')
                def foo(value):
                    return value

                depends('--help')(foo)
            """
            ):
                self.get_config()

        self.assertEqual(str(e.exception), "Cannot nest @depends functions")

        with self.assertRaises(TypeError) as e:
            with self.moz_configure(
                """
                @template
                def foo(f):
                    pass

                depends('--help')(foo)
            """
            ):
                self.get_config()

        self.assertEqual(str(e.exception), "Cannot use a @template function here")

        with self.assertRaises(ConfigureError) as e:
            with self.moz_configure(
                """
                option('--foo', help='Foo')
                @depends('--foo')
                def foo(value):
                    return value

                foo()
            """
            ):
                self.get_config()

        self.assertEqual(str(e.exception), "The `foo` function may not be called")

        with self.assertRaises(TypeError) as e:
            with self.moz_configure(
                """
                @depends('--help', foo=42)
                def foo(_):
                    return
            """
            ):
                self.get_config()

        self.assertEqual(
            str(e.exception), "depends_impl() got an unexpected keyword argument 'foo'"
        )

    def test_depends_when(self):
        with self.moz_configure(
            """
            @depends(when=True)
            def foo():
                return 'foo'

            set_config('FOO', foo)

            @depends(when=False)
            def bar():
                return 'bar'

            set_config('BAR', bar)

            option('--with-qux', help='Qux')
            @depends(when='--with-qux')
            def qux():
                return 'qux'

            set_config('QUX', qux)
        """
        ):
            config = self.get_config()
            self.assertEqual(
                config,
                {
                    "FOO": "foo",
                },
            )

            config = self.get_config(["--with-qux"])
            self.assertEqual(
                config,
                {
                    "FOO": "foo",
                    "QUX": "qux",
                },
            )

    def test_depends_value(self):
        with self.moz_configure(
            """
            foo = depends(when=True)('foo')

            set_config('FOO', foo)

            bar = depends(when=False)('bar')

            set_config('BAR', bar)

            option('--with-qux', help='Qux')
            @depends(when='--with-qux')
            def qux():
                return 'qux'

            set_config('QUX', qux)
        """
        ):
            config = self.get_config()
            self.assertEqual(
                config,
                {
                    "FOO": "foo",
                },
            )

        with self.assertRaises(TypeError) as e:
            with self.moz_configure(
                """
                option('--foo', help='Foo')

                depends('--foo')('foo')
            """
            ):
                self.get_config()

        self.assertEqual(
            str(e.exception), "Cannot wrap literal values in @depends with dependencies"
        )

    def test_imports_failures(self):
        with self.assertRaises(ConfigureError) as e:
            with self.moz_configure(
                """
                @imports('os')
                @template
                def foo(value):
                    return value
            """
            ):
                self.get_config()

        self.assertEqual(str(e.exception), "@imports must appear after @template")

        with self.assertRaises(ConfigureError) as e:
            with self.moz_configure(
                """
                option('--foo', help='Foo')
                @imports('os')
                @depends('--foo')
                def foo(value):
                    return value
            """
            ):
                self.get_config()

        self.assertEqual(str(e.exception), "@imports must appear after @depends")

        for import_ in (
            "42",
            "_from=42, _import='os'",
            "_from='os', _import='path', _as=42",
        ):
            with self.assertRaises(TypeError) as e:
                with self.moz_configure(
                    """
                    @imports(%s)
                    @template
                    def foo(value):
                        return value
                """
                    % import_
                ):
                    self.get_config()

            self.assertEqual(str(e.exception), "Unexpected type: 'int'")

        with self.assertRaises(TypeError) as e:
            with self.moz_configure(
                """
                @imports('os', 42)
                @template
                def foo(value):
                    return value
            """
            ):
                self.get_config()

        self.assertEqual(str(e.exception), "Unexpected type: 'int'")

        with self.assertRaises(ValueError) as e:
            with self.moz_configure(
                """
                @imports('os*')
                def foo(value):
                    return value
            """
            ):
                self.get_config()

        self.assertEqual(str(e.exception), "Invalid argument to @imports: 'os*'")

    def test_only_when(self):
        moz_configure = """
            option('--enable-when', help='When')
            @depends('--enable-when', '--help')
            def when(value, _):
                return bool(value)

            with only_when(when):
                option('--foo', nargs='*', help='Foo')
                @depends('--foo')
                def foo(value):
                    return value

                set_config('FOO', foo)
                set_define('FOO', foo)

            # It is possible to depend on a function defined in a only_when
            # block. It then resolves to `None`.
            set_config('BAR', depends(foo)(lambda x: x))
            set_define('BAR', depends(foo)(lambda x: x))
        """

        with self.moz_configure(moz_configure):
            config = self.get_config()
            self.assertEqual(
                config,
                {
                    "DEFINES": {},
                },
            )

            config = self.get_config(["--enable-when"])
            self.assertEqual(
                config,
                {
                    "BAR": NegativeOptionValue(),
                    "FOO": NegativeOptionValue(),
                    "DEFINES": {
                        "BAR": NegativeOptionValue(),
                        "FOO": NegativeOptionValue(),
                    },
                },
            )

            config = self.get_config(["--enable-when", "--foo=bar"])
            self.assertEqual(
                config,
                {
                    "BAR": PositiveOptionValue(["bar"]),
                    "FOO": PositiveOptionValue(["bar"]),
                    "DEFINES": {
                        "BAR": PositiveOptionValue(["bar"]),
                        "FOO": PositiveOptionValue(["bar"]),
                    },
                },
            )

            # The --foo option doesn't exist when --enable-when is not given.
            with self.assertRaises(InvalidOptionError) as e:
                self.get_config(["--foo"])

            self.assertEqual(
                str(e.exception), "--foo is not available in this configuration"
            )

        # Cannot depend on an option defined in a only_when block, because we
        # don't know what OptionValue would make sense.
        with self.moz_configure(
            moz_configure
            + """
            set_config('QUX', depends('--foo')(lambda x: x))
        """
        ):
            with self.assertRaises(ConfigureError) as e:
                self.get_config()

            self.assertEqual(
                str(e.exception),
                "@depends function needs the same `when` as " "options it depends on",
            )

        with self.moz_configure(
            moz_configure
            + """
            set_config('QUX', depends('--foo', when=when)(lambda x: x))
        """
        ):
            self.get_config(["--enable-when"])

        # Using imply_option for an option defined in a only_when block fails
        # similarly if the imply_option happens outside the block.
        with self.moz_configure(
            """
            imply_option('--foo', True)
        """
            + moz_configure
        ):
            with self.assertRaises(InvalidOptionError) as e:
                self.get_config()

            self.assertEqual(
                str(e.exception), "--foo is not available in this configuration"
            )

        # And similarly doesn't fail when the condition is true.
        with self.moz_configure(
            """
            imply_option('--foo', True)
        """
            + moz_configure
        ):
            self.get_config(["--enable-when"])

    def test_depends_unary_ops_func(self):
        with self.moz_configure(
            """
            option('--foo', nargs=1, help='Foo')
            @depends('--foo')
            def foo(value):
                return value
            set_config('Foo', foo)
            set_config('notFoo', depends(foo)(lambda x: not x))
            set_config('invFoo', ~foo)
        """
        ):
            foo_opt, foo_value = "--foo=foo", PositiveOptionValue(("foo",))

            config = self.get_config([foo_opt])
            self.assertEqual(
                config,
                {
                    "Foo": foo_value,
                    "notFoo": not foo_value,
                    "invFoo": not foo_value,
                },
            )

            foo_value = False
            config = self.get_config([])
            self.assertEqual(
                config,
                {
                    "Foo": foo_value,
                    "notFoo": not foo_value,
                    "invFoo": not foo_value,
                },
            )

    def test_depends_unary_ops_val(self):
        with self.moz_configure(
            """
            option("--cond", help="Condition")
            cond = depends("--cond")(lambda c: c)
            foo = depends(when=cond)("foo")
            set_config('Foo', foo)
            set_config('notFoo', depends(foo)(lambda x: not x))
            set_config('invFoo', ~foo)

            bar = depends(when=~cond)("bar")
            bar2 = depends(when=depends(cond)(lambda c: not c))("bar2")
            set_config('Bar', bar)
            set_config('Bar2', bar2)
        """
        ):
            config = self.get_config(["--cond"])
            self.assertEqual(
                config,
                {
                    "Foo": "foo",
                    "notFoo": not "foo",
                    "invFoo": not "foo",
                },
            )
            config = self.get_config([])
            self.assertEqual(
                config,
                {"notFoo": True, "invFoo": True, "Bar": "bar", "Bar2": "bar2"},
            )

    def test_depends_binary_ops(self):
        with self.moz_configure(
            """
            option('--foo', nargs=1, help='Foo')
            @depends('--foo')
            def foo(value):
                return value or 0

            option('--bar', nargs=1, help='Bar')
            @depends('--bar')
            def bar(value):
                return value or ''

            option('--baz', nargs=1, help='Baz')
            @depends('--baz')
            def baz(value):
                return value

            set_config('FOOorBAR', foo | bar)
            set_config('FOOorBARorBAZ', foo | bar | baz)
            set_config('FOOandBAR', foo & bar)
            set_config('FOOandBARandBAZ', foo & bar & baz)
        """
        ):
            for foo_opt, foo_value in (
                ("", 0),
                ("--foo=foo", PositiveOptionValue(("foo",))),
            ):
                for bar_opt, bar_value in (
                    ("", ""),
                    ("--bar=bar", PositiveOptionValue(("bar",))),
                ):
                    for baz_opt, baz_value in (
                        ("", NegativeOptionValue()),
                        ("--baz=baz", PositiveOptionValue(("baz",))),
                    ):
                        config = self.get_config(
                            [x for x in (foo_opt, bar_opt, baz_opt) if x]
                        )
                        self.assertEqual(
                            config,
                            {
                                "FOOorBAR": foo_value or bar_value,
                                "FOOorBARorBAZ": foo_value or bar_value or baz_value,
                                "FOOandBAR": foo_value and bar_value,
                                "FOOandBARandBAZ": foo_value
                                and bar_value
                                and baz_value,
                            },
                        )

    def test_depends_getattr(self):
        with self.moz_configure(
            """
            option('--foo', nargs=1, help='Foo')
            @depends('--foo')
            def foo(value):
                return value

            option('--bar', nargs=1, help='Bar')
            @depends('--bar')
            def bar(value):
                return value or None

            @depends(foo, bar)
            def foobar(foo, bar):
                return namespace(foo=foo, bar=bar)

            set_config('FOO', foobar.foo)
            set_config('BAR', foobar.bar)
            set_config('BAZ', foobar.baz)
        """
        ):
            config = self.get_config()
            self.assertEqual(
                config,
                {
                    "FOO": NegativeOptionValue(),
                },
            )

            config = self.get_config(["--foo=foo"])
            self.assertEqual(
                config,
                {
                    "FOO": PositiveOptionValue(("foo",)),
                },
            )

            config = self.get_config(["--bar=bar"])
            self.assertEqual(
                config,
                {
                    "FOO": NegativeOptionValue(),
                    "BAR": PositiveOptionValue(("bar",)),
                },
            )

            config = self.get_config(["--foo=foo", "--bar=bar"])
            self.assertEqual(
                config,
                {
                    "FOO": PositiveOptionValue(("foo",)),
                    "BAR": PositiveOptionValue(("bar",)),
                },
            )


if __name__ == "__main__":
    main()
