# 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 unittest

from mozunit import main

from mozbuild.configure.options import (
    CommandLineHelper,
    ConflictingOptionError,
    InvalidOptionError,
    NegativeOptionValue,
    Option,
    OptionValue,
    PositiveOptionValue,
)


class Option(Option):
    def __init__(self, *args, **kwargs):
        kwargs["help"] = "Dummy help"
        super(Option, self).__init__(*args, **kwargs)


class TestOption(unittest.TestCase):
    def test_option(self):
        option = Option("--option")
        self.assertEqual(option.prefix, "")
        self.assertEqual(option.name, "option")
        self.assertEqual(option.env, None)
        self.assertFalse(option.default)

        option = Option("--enable-option")
        self.assertEqual(option.prefix, "enable")
        self.assertEqual(option.name, "option")
        self.assertEqual(option.env, None)
        self.assertFalse(option.default)

        option = Option("--disable-option")
        self.assertEqual(option.prefix, "disable")
        self.assertEqual(option.name, "option")
        self.assertEqual(option.env, None)
        self.assertTrue(option.default)

        option = Option("--with-option")
        self.assertEqual(option.prefix, "with")
        self.assertEqual(option.name, "option")
        self.assertEqual(option.env, None)
        self.assertFalse(option.default)

        option = Option("--without-option")
        self.assertEqual(option.prefix, "without")
        self.assertEqual(option.name, "option")
        self.assertEqual(option.env, None)
        self.assertTrue(option.default)

        option = Option("--without-option-foo", env="MOZ_OPTION")
        self.assertEqual(option.env, "MOZ_OPTION")

        option = Option(env="MOZ_OPTION")
        self.assertEqual(option.prefix, "")
        self.assertEqual(option.name, None)
        self.assertEqual(option.env, "MOZ_OPTION")
        self.assertFalse(option.default)

        with self.assertRaises(InvalidOptionError) as e:
            Option("--option", nargs=0, default=("a",))
        self.assertEqual(
            str(e.exception), "The given `default` doesn't satisfy `nargs`"
        )

        with self.assertRaises(InvalidOptionError) as e:
            Option("--option", nargs=1, default=())
        self.assertEqual(
            str(e.exception), "default must be a bool, a string or a tuple of strings"
        )

        with self.assertRaises(InvalidOptionError) as e:
            Option("--option", nargs=1, default=True)
        self.assertEqual(
            str(e.exception), "The given `default` doesn't satisfy `nargs`"
        )

        with self.assertRaises(InvalidOptionError) as e:
            Option("--option", nargs=1, default=("a", "b"))
        self.assertEqual(
            str(e.exception), "The given `default` doesn't satisfy `nargs`"
        )

        with self.assertRaises(InvalidOptionError) as e:
            Option("--option", nargs=2, default=())
        self.assertEqual(
            str(e.exception), "default must be a bool, a string or a tuple of strings"
        )

        with self.assertRaises(InvalidOptionError) as e:
            Option("--option", nargs=2, default=True)
        self.assertEqual(
            str(e.exception), "The given `default` doesn't satisfy `nargs`"
        )

        with self.assertRaises(InvalidOptionError) as e:
            Option("--option", nargs=2, default=("a",))
        self.assertEqual(
            str(e.exception), "The given `default` doesn't satisfy `nargs`"
        )

        with self.assertRaises(InvalidOptionError) as e:
            Option("--option", nargs="?", default=("a", "b"))
        self.assertEqual(
            str(e.exception), "The given `default` doesn't satisfy `nargs`"
        )

        with self.assertRaises(InvalidOptionError) as e:
            Option("--option", nargs="+", default=())
        self.assertEqual(
            str(e.exception), "default must be a bool, a string or a tuple of strings"
        )

        with self.assertRaises(InvalidOptionError) as e:
            Option("--option", nargs="+", default=True)
        self.assertEqual(
            str(e.exception), "The given `default` doesn't satisfy `nargs`"
        )

        # --disable options with a nargs value that requires at least one
        # argument need to be given a default.
        with self.assertRaises(InvalidOptionError) as e:
            Option("--disable-option", nargs=1)
        self.assertEqual(
            str(e.exception), "The given `default` doesn't satisfy `nargs`"
        )

        with self.assertRaises(InvalidOptionError) as e:
            Option("--disable-option", nargs="+")
        self.assertEqual(
            str(e.exception), "The given `default` doesn't satisfy `nargs`"
        )

        # Test nargs inference from default value
        option = Option("--with-foo", default=True)
        self.assertEqual(option.nargs, 0)

        option = Option("--with-foo", default=False)
        self.assertEqual(option.nargs, 0)

        option = Option("--with-foo", default="a")
        self.assertEqual(option.nargs, "?")

        option = Option("--with-foo", default=("a",))
        self.assertEqual(option.nargs, "?")

        option = Option("--with-foo", default=("a", "b"))
        self.assertEqual(option.nargs, "*")

        option = Option(env="FOO", default=True)
        self.assertEqual(option.nargs, 0)

        option = Option(env="FOO", default=False)
        self.assertEqual(option.nargs, 0)

        option = Option(env="FOO", default="a")
        self.assertEqual(option.nargs, "?")

        option = Option(env="FOO", default=("a",))
        self.assertEqual(option.nargs, "?")

        option = Option(env="FOO", default=("a", "b"))
        self.assertEqual(option.nargs, "*")

    def test_option_option(self):
        for option in (
            "--option",
            "--enable-option",
            "--disable-option",
            "--with-option",
            "--without-option",
        ):
            self.assertEqual(Option(option).option, option)
            self.assertEqual(Option(option, env="FOO").option, option)

            opt = Option(option, default=False)
            self.assertEqual(
                opt.option,
                option.replace("-disable-", "-enable-").replace("-without-", "-with-"),
            )

            opt = Option(option, default=True)
            self.assertEqual(
                opt.option,
                option.replace("-enable-", "-disable-").replace("-with-", "-without-"),
            )

        self.assertEqual(Option(env="FOO").option, "FOO")

    def test_option_choices(self):
        with self.assertRaises(InvalidOptionError) as e:
            Option("--option", nargs=3, choices=("a", "b"))
        self.assertEqual(str(e.exception), "Not enough `choices` for `nargs`")

        with self.assertRaises(InvalidOptionError) as e:
            Option("--without-option", nargs=1, choices=("a", "b"))
        self.assertEqual(
            str(e.exception), "A `default` must be given along with `choices`"
        )

        with self.assertRaises(InvalidOptionError) as e:
            Option("--without-option", nargs="+", choices=("a", "b"))
        self.assertEqual(
            str(e.exception), "A `default` must be given along with `choices`"
        )

        with self.assertRaises(InvalidOptionError) as e:
            Option("--without-option", default="c", choices=("a", "b"))
        self.assertEqual(
            str(e.exception), "The `default` value must be one of 'a', 'b'"
        )

        with self.assertRaises(InvalidOptionError) as e:
            Option(
                "--without-option",
                default=(
                    "a",
                    "c",
                ),
                choices=("a", "b"),
            )
        self.assertEqual(
            str(e.exception), "The `default` value must be one of 'a', 'b'"
        )

        with self.assertRaises(InvalidOptionError) as e:
            Option("--without-option", default=("c",), choices=("a", "b"))
        self.assertEqual(
            str(e.exception), "The `default` value must be one of 'a', 'b'"
        )

        option = Option("--with-option", nargs="+", choices=("a", "b"))
        with self.assertRaises(InvalidOptionError) as e:
            option.get_value("--with-option=c")
        self.assertEqual(str(e.exception), "'c' is not one of 'a', 'b'")

        value = option.get_value("--with-option=b,a")
        self.assertTrue(value)
        self.assertEqual(PositiveOptionValue(("b", "a")), value)

        option = Option("--without-option", nargs="*", default="a", choices=("a", "b"))
        with self.assertRaises(InvalidOptionError) as e:
            option.get_value("--with-option=c")
        self.assertEqual(str(e.exception), "'c' is not one of 'a', 'b'")

        value = option.get_value("--with-option=b,a")
        self.assertTrue(value)
        self.assertEqual(PositiveOptionValue(("b", "a")), value)

        # Default is enabled without a value, but the option can be also be disabled or
        # used with a value.
        option = Option("--without-option", nargs="*", choices=("a", "b"))
        value = option.get_value("--with-option")
        self.assertEqual(PositiveOptionValue(), value)
        value = option.get_value("--with-option=a")
        self.assertEqual(PositiveOptionValue(("a",)), value)
        with self.assertRaises(InvalidOptionError) as e:
            option.get_value("--with-option=c")
        self.assertEqual(str(e.exception), "'c' is not one of 'a', 'b'")

        # Test nargs inference from choices
        option = Option("--with-option", choices=("a", "b"))
        self.assertEqual(option.nargs, 1)

        # Test "relative" values
        option = Option(
            "--with-option", nargs="*", default=("b", "c"), choices=("a", "b", "c", "d")
        )

        value = option.get_value("--with-option=+d")
        self.assertEqual(PositiveOptionValue(("b", "c", "d")), value)

        value = option.get_value("--with-option=-b")
        self.assertEqual(PositiveOptionValue(("c",)), value)

        value = option.get_value("--with-option=-b,+d")
        self.assertEqual(PositiveOptionValue(("c", "d")), value)

        # Adding something that is in the default is fine
        value = option.get_value("--with-option=+b")
        self.assertEqual(PositiveOptionValue(("b", "c")), value)

        # Removing something that is not in the default is fine, as long as it
        # is one of the choices
        value = option.get_value("--with-option=-a")
        self.assertEqual(PositiveOptionValue(("b", "c")), value)

        with self.assertRaises(InvalidOptionError) as e:
            option.get_value("--with-option=-e")
        self.assertEqual(str(e.exception), "'e' is not one of 'a', 'b', 'c', 'd'")

        # Other "not a choice" errors.
        with self.assertRaises(InvalidOptionError) as e:
            option.get_value("--with-option=+e")
        self.assertEqual(str(e.exception), "'e' is not one of 'a', 'b', 'c', 'd'")

        with self.assertRaises(InvalidOptionError) as e:
            option.get_value("--with-option=e")
        self.assertEqual(str(e.exception), "'e' is not one of 'a', 'b', 'c', 'd'")

    def test_option_value_compare(self):
        # OptionValue are tuple and equivalence should compare as tuples.
        val = PositiveOptionValue(("foo",))

        self.assertEqual(val[0], "foo")
        self.assertEqual(val, PositiveOptionValue(("foo",)))
        self.assertNotEqual(val, PositiveOptionValue(("foo", "bar")))

        # Can compare a tuple to an OptionValue.
        self.assertEqual(val, ("foo",))
        self.assertNotEqual(val, ("foo", "bar"))

        # Different OptionValue types are never equal.
        self.assertNotEqual(val, OptionValue(("foo",)))

        # For usability reasons, we raise TypeError when attempting to compare
        # against a non-tuple.
        with self.assertRaisesRegex(TypeError, "cannot compare a"):
            val == "foo"

        # But we allow empty option values to compare otherwise we can't
        # easily compare value-less types like PositiveOptionValue and
        # NegativeOptionValue.
        empty_positive = PositiveOptionValue()
        empty_negative = NegativeOptionValue()
        self.assertEqual(empty_positive, ())
        self.assertEqual(empty_positive, PositiveOptionValue())
        self.assertEqual(empty_negative, ())
        self.assertEqual(empty_negative, NegativeOptionValue())
        self.assertNotEqual(empty_positive, "foo")
        self.assertNotEqual(empty_positive, ("foo",))
        self.assertNotEqual(empty_negative, "foo")
        self.assertNotEqual(empty_negative, ("foo",))

    def test_option_value_format(self):
        val = PositiveOptionValue()
        self.assertEqual("--with-value", val.format("--with-value"))
        self.assertEqual("--with-value", val.format("--without-value"))
        self.assertEqual("--enable-value", val.format("--enable-value"))
        self.assertEqual("--enable-value", val.format("--disable-value"))
        self.assertEqual("--value", val.format("--value"))
        self.assertEqual("VALUE=1", val.format("VALUE"))

        val = PositiveOptionValue(("a",))
        self.assertEqual("--with-value=a", val.format("--with-value"))
        self.assertEqual("--with-value=a", val.format("--without-value"))
        self.assertEqual("--enable-value=a", val.format("--enable-value"))
        self.assertEqual("--enable-value=a", val.format("--disable-value"))
        self.assertEqual("--value=a", val.format("--value"))
        self.assertEqual("VALUE=a", val.format("VALUE"))

        val = PositiveOptionValue(("a", "b"))
        self.assertEqual("--with-value=a,b", val.format("--with-value"))
        self.assertEqual("--with-value=a,b", val.format("--without-value"))
        self.assertEqual("--enable-value=a,b", val.format("--enable-value"))
        self.assertEqual("--enable-value=a,b", val.format("--disable-value"))
        self.assertEqual("--value=a,b", val.format("--value"))
        self.assertEqual("VALUE=a,b", val.format("VALUE"))

        val = NegativeOptionValue()
        self.assertEqual("--without-value", val.format("--with-value"))
        self.assertEqual("--without-value", val.format("--without-value"))
        self.assertEqual("--disable-value", val.format("--enable-value"))
        self.assertEqual("--disable-value", val.format("--disable-value"))
        self.assertEqual("", val.format("--value"))
        self.assertEqual("VALUE=", val.format("VALUE"))

    def test_option_value(self, name="option", nargs=0, default=None):
        disabled = name.startswith(("disable-", "without-"))
        if disabled:
            negOptionValue = PositiveOptionValue
            posOptionValue = NegativeOptionValue
        else:
            posOptionValue = PositiveOptionValue
            negOptionValue = NegativeOptionValue
        defaultValue = PositiveOptionValue(default) if default else negOptionValue()

        option = Option("--%s" % name, nargs=nargs, default=default)

        if nargs in (0, "?", "*") or disabled:
            value = option.get_value("--%s" % name, "option")
            self.assertEqual(value, posOptionValue())
            self.assertEqual(value.origin, "option")
        else:
            with self.assertRaises(InvalidOptionError) as e:
                option.get_value("--%s" % name)
            if nargs == 1:
                self.assertEqual(str(e.exception), "--%s takes 1 value" % name)
            elif nargs == "+":
                self.assertEqual(str(e.exception), "--%s takes 1 or more values" % name)
            else:
                self.assertEqual(str(e.exception), "--%s takes 2 values" % name)

        value = option.get_value("")
        self.assertEqual(value, defaultValue)
        self.assertEqual(value.origin, "default")

        value = option.get_value(None)
        self.assertEqual(value, defaultValue)
        self.assertEqual(value.origin, "default")

        with self.assertRaises(AssertionError):
            value = option.get_value("MOZ_OPTION=", "environment")

        with self.assertRaises(AssertionError):
            value = option.get_value("MOZ_OPTION=1", "environment")

        with self.assertRaises(AssertionError):
            value = option.get_value("--foo")

        if nargs in (1, "?", "*", "+") and not disabled:
            value = option.get_value("--%s=" % name, "option")
            self.assertEqual(value, PositiveOptionValue(("",)))
            self.assertEqual(value.origin, "option")
        else:
            with self.assertRaises(InvalidOptionError) as e:
                option.get_value("--%s=" % name)
            if disabled:
                self.assertEqual(str(e.exception), "Cannot pass a value to --%s" % name)
            else:
                self.assertEqual(
                    str(e.exception), "--%s takes %d values" % (name, nargs)
                )

        if nargs in (1, "?", "*", "+") and not disabled:
            value = option.get_value("--%s=foo" % name, "option")
            self.assertEqual(value, PositiveOptionValue(("foo",)))
            self.assertEqual(value.origin, "option")
        else:
            with self.assertRaises(InvalidOptionError) as e:
                option.get_value("--%s=foo" % name)
            if disabled:
                self.assertEqual(str(e.exception), "Cannot pass a value to --%s" % name)
            else:
                self.assertEqual(
                    str(e.exception), "--%s takes %d values" % (name, nargs)
                )

        if nargs in (1, "?", 2, "*", "+") and not disabled:
            value = option.get_value("--%s=foo,bar" % name, "option")
            if nargs in (1, "?"):
                self.assertEqual(value, PositiveOptionValue(("foo,bar",)))
            else:
                self.assertEqual(value, PositiveOptionValue(("foo", "bar")))
            self.assertEqual(value.origin, "option")
        else:
            with self.assertRaises(InvalidOptionError) as e:
                option.get_value("--%s=foo,bar" % name, "option")
            if disabled:
                self.assertEqual(str(e.exception), "Cannot pass a value to --%s" % name)
            else:
                self.assertEqual(
                    str(e.exception),
                    "--%s takes %d values" % (name, nargs),
                )

        option = Option("--%s" % name, env="MOZ_OPTION", nargs=nargs, default=default)
        if nargs in (0, "?", "*") or disabled:
            value = option.get_value("--%s" % name, "option")
            self.assertEqual(value, posOptionValue())
            self.assertEqual(value.origin, "option")
        else:
            with self.assertRaises(InvalidOptionError) as e:
                option.get_value("--%s" % name)
            if disabled:
                self.assertEqual(str(e.exception), "Cannot pass a value to --%s" % name)
            elif nargs == "+":
                self.assertEqual(str(e.exception), "--%s takes 1 or more values" % name)
            else:
                self.assertEqual(
                    str(e.exception),
                    "--%s takes %d value%s" % (name, nargs, "s" if nargs != 1 else ""),
                )

        value = option.get_value("")
        self.assertEqual(value, defaultValue)
        self.assertEqual(value.origin, "default")

        value = option.get_value(None)
        self.assertEqual(value, defaultValue)
        self.assertEqual(value.origin, "default")

        value = option.get_value("MOZ_OPTION=", "environment")
        self.assertEqual(value, NegativeOptionValue())
        self.assertEqual(value.origin, "environment")

        if nargs in (0, "?", "*"):
            value = option.get_value("MOZ_OPTION=1", "environment")
            self.assertEqual(value, PositiveOptionValue())
            self.assertEqual(value.origin, "environment")
        elif nargs in (1, "+"):
            value = option.get_value("MOZ_OPTION=1", "environment")
            self.assertEqual(value, PositiveOptionValue(("1",)))
            self.assertEqual(value.origin, "environment")
        else:
            with self.assertRaises(InvalidOptionError) as e:
                option.get_value("MOZ_OPTION=1", "environment")
            self.assertEqual(str(e.exception), "MOZ_OPTION takes 2 values")

        if nargs in (1, "?", "*", "+") and not disabled:
            value = option.get_value("--%s=" % name, "option")
            self.assertEqual(value, PositiveOptionValue(("",)))
            self.assertEqual(value.origin, "option")
        else:
            with self.assertRaises(InvalidOptionError) as e:
                option.get_value("--%s=" % name, "option")
            if disabled:
                self.assertEqual(str(e.exception), "Cannot pass a value to --%s" % name)
            else:
                self.assertEqual(
                    str(e.exception), "--%s takes %d values" % (name, nargs)
                )

        with self.assertRaises(AssertionError):
            value = option.get_value("--foo", "option")

        if nargs in (1, "?", "*", "+"):
            value = option.get_value("MOZ_OPTION=foo", "environment")
            self.assertEqual(value, PositiveOptionValue(("foo",)))
            self.assertEqual(value.origin, "environment")
        else:
            with self.assertRaises(InvalidOptionError) as e:
                option.get_value("MOZ_OPTION=foo", "environment")
            self.assertEqual(str(e.exception), "MOZ_OPTION takes %d values" % nargs)

        if nargs in (1, "?", 2, "*", "+"):
            value = option.get_value("MOZ_OPTION=foo,bar", "environment")
            if nargs in (1, "?"):
                self.assertEqual(value, PositiveOptionValue(("foo,bar",)))
            else:
                self.assertEqual(value, PositiveOptionValue(("foo", "bar")))
            self.assertEqual(value.origin, "environment")
        else:
            with self.assertRaises(InvalidOptionError) as e:
                option.get_value("MOZ_OPTION=foo,bar", "environment")
            self.assertEqual(
                str(e.exception),
                "MOZ_OPTION takes %d values" % nargs,
            )

        if disabled:
            return option

        env_option = Option(env="MOZ_OPTION", nargs=nargs, default=default)
        with self.assertRaises(AssertionError):
            env_option.get_value("--%s" % name)

        value = env_option.get_value("")
        self.assertEqual(value, defaultValue)
        self.assertEqual(value.origin, "default")

        value = env_option.get_value("MOZ_OPTION=", "environment")
        self.assertEqual(value, negOptionValue())
        self.assertEqual(value.origin, "environment")

        if nargs in (0, "?", "*"):
            value = env_option.get_value("MOZ_OPTION=1", "environment")
            self.assertEqual(value, posOptionValue())
            self.assertTrue(value)
            self.assertEqual(value.origin, "environment")
        elif nargs in (1, "+"):
            value = env_option.get_value("MOZ_OPTION=1", "environment")
            self.assertEqual(value, PositiveOptionValue(("1",)))
            self.assertEqual(value.origin, "environment")
        else:
            with self.assertRaises(InvalidOptionError) as e:
                env_option.get_value("MOZ_OPTION=1", "environment")
            self.assertEqual(str(e.exception), "MOZ_OPTION takes 2 values")

        with self.assertRaises(AssertionError) as e:
            env_option.get_value("--%s" % name)

        with self.assertRaises(AssertionError) as e:
            env_option.get_value("--foo")

        if nargs in (1, "?", "*", "+"):
            value = env_option.get_value("MOZ_OPTION=foo", "environment")
            self.assertEqual(value, PositiveOptionValue(("foo",)))
            self.assertEqual(value.origin, "environment")
        else:
            with self.assertRaises(InvalidOptionError) as e:
                env_option.get_value("MOZ_OPTION=foo", "environment")
            self.assertEqual(str(e.exception), "MOZ_OPTION takes %d values" % nargs)

        if nargs in (1, "?", 2, "*", "+"):
            value = env_option.get_value("MOZ_OPTION=foo,bar", "environment")
            if nargs in (1, "?"):
                self.assertEqual(value, PositiveOptionValue(("foo,bar",)))
            else:
                self.assertEqual(value, PositiveOptionValue(("foo", "bar")))
            self.assertEqual(value.origin, "environment")
        else:
            with self.assertRaises(InvalidOptionError) as e:
                env_option.get_value("MOZ_OPTION=foo,bar", "environment")
            self.assertEqual(
                str(e.exception),
                "MOZ_OPTION takes %d values" % nargs,
            )

        return option

    def test_option_value_enable(
        self, enable="enable", disable="disable", nargs=0, default=None
    ):
        option = self.test_option_value(
            "%s-option" % enable, nargs=nargs, default=default
        )

        value = option.get_value("--%s-option" % disable, "option")
        self.assertEqual(value, NegativeOptionValue())
        self.assertEqual(value.origin, "option")

        option = self.test_option_value(
            "%s-option" % disable, nargs=nargs, default=default
        )

        if nargs in (0, "?", "*"):
            value = option.get_value("--%s-option" % enable, "option")
            self.assertEqual(value, PositiveOptionValue())
            self.assertEqual(value.origin, "option")
        else:
            with self.assertRaises(InvalidOptionError) as e:
                option.get_value("--%s-option" % enable, "option")
            if nargs == 1:
                self.assertEqual(str(e.exception), "--%s-option takes 1 value" % enable)
            elif nargs == "+":
                self.assertEqual(
                    str(e.exception), "--%s-option takes 1 or more values" % enable
                )
            else:
                self.assertEqual(
                    str(e.exception), "--%s-option takes 2 values" % enable
                )

    def test_option_value_with(self):
        self.test_option_value_enable("with", "without")

    def test_option_value_invalid_nargs(self):
        with self.assertRaises(InvalidOptionError) as e:
            Option("--option", nargs="foo")
        self.assertEqual(
            str(e.exception), "nargs must be a positive integer, '?', '*' or '+'"
        )

        with self.assertRaises(InvalidOptionError) as e:
            Option("--option", nargs=-2)
        self.assertEqual(
            str(e.exception), "nargs must be a positive integer, '?', '*' or '+'"
        )

    def test_option_value_nargs_1(self):
        self.test_option_value(nargs=1)
        self.test_option_value(nargs=1, default=("a",))
        self.test_option_value_enable(nargs=1, default=("a",))

        # A default is required
        with self.assertRaises(InvalidOptionError) as e:
            Option("--disable-option", nargs=1)
        self.assertEqual(
            str(e.exception), "The given `default` doesn't satisfy `nargs`"
        )

    def test_option_value_nargs_2(self):
        self.test_option_value(nargs=2)
        self.test_option_value(nargs=2, default=("a", "b"))
        self.test_option_value_enable(nargs=2, default=("a", "b"))

        # A default is required
        with self.assertRaises(InvalidOptionError) as e:
            Option("--disable-option", nargs=2)
        self.assertEqual(
            str(e.exception), "The given `default` doesn't satisfy `nargs`"
        )

    def test_option_value_nargs_0_or_1(self):
        self.test_option_value(nargs="?")
        self.test_option_value(nargs="?", default=("a",))
        self.test_option_value_enable(nargs="?")
        self.test_option_value_enable(nargs="?", default=("a",))

    def test_option_value_nargs_0_or_more(self):
        self.test_option_value(nargs="*")
        self.test_option_value(nargs="*", default=("a",))
        self.test_option_value(nargs="*", default=("a", "b"))
        self.test_option_value_enable(nargs="*")
        self.test_option_value_enable(nargs="*", default=("a",))
        self.test_option_value_enable(nargs="*", default=("a", "b"))

    def test_option_value_nargs_1_or_more(self):
        self.test_option_value(nargs="+")
        self.test_option_value(nargs="+", default=("a",))
        self.test_option_value(nargs="+", default=("a", "b"))
        self.test_option_value_enable(nargs="+", default=("a",))
        self.test_option_value_enable(nargs="+", default=("a", "b"))

        # A default is required
        with self.assertRaises(InvalidOptionError) as e:
            Option("--disable-option", nargs="+")
        self.assertEqual(
            str(e.exception), "The given `default` doesn't satisfy `nargs`"
        )


class TestCommandLineHelper(unittest.TestCase):
    def test_basic(self):
        helper = CommandLineHelper({}, ["cmd", "--foo", "--bar"])

        self.assertEqual(["--foo", "--bar"], list(helper))

        helper.add("--enable-qux")

        self.assertEqual(["--foo", "--bar", "--enable-qux"], list(helper))

        value, option = helper.handle(Option("--bar"))
        self.assertEqual(["--foo", "--enable-qux"], list(helper))
        self.assertEqual(PositiveOptionValue(), value)
        self.assertEqual("--bar", option)

        value, option = helper.handle(Option("--baz"))
        self.assertEqual(["--foo", "--enable-qux"], list(helper))
        self.assertEqual(NegativeOptionValue(), value)
        self.assertEqual(None, option)

        with self.assertRaises(AssertionError):
            CommandLineHelper({}, ["--foo", "--bar"])

    def test_precedence(self):
        foo = Option("--with-foo", nargs="*")
        helper = CommandLineHelper({}, ["cmd", "--with-foo=a,b"])
        value, option = helper.handle(foo)
        self.assertEqual(PositiveOptionValue(("a", "b")), value)
        self.assertEqual("command-line", value.origin)
        self.assertEqual("--with-foo=a,b", option)

        helper = CommandLineHelper({}, ["cmd", "--with-foo=a,b", "--without-foo"])
        value, option = helper.handle(foo)
        self.assertEqual(NegativeOptionValue(), value)
        self.assertEqual("command-line", value.origin)
        self.assertEqual("--without-foo", option)

        helper = CommandLineHelper({}, ["cmd", "--without-foo", "--with-foo=a,b"])
        value, option = helper.handle(foo)
        self.assertEqual(PositiveOptionValue(("a", "b")), value)
        self.assertEqual("command-line", value.origin)
        self.assertEqual("--with-foo=a,b", option)

        foo = Option("--with-foo", env="FOO", nargs="*")
        helper = CommandLineHelper({"FOO": ""}, ["cmd", "--with-foo=a,b"])
        value, option = helper.handle(foo)
        self.assertEqual(PositiveOptionValue(("a", "b")), value)
        self.assertEqual("command-line", value.origin)
        self.assertEqual("--with-foo=a,b", option)

        helper = CommandLineHelper({"FOO": "a,b"}, ["cmd", "--without-foo"])
        value, option = helper.handle(foo)
        self.assertEqual(NegativeOptionValue(), value)
        self.assertEqual("command-line", value.origin)
        self.assertEqual("--without-foo", option)

        helper = CommandLineHelper({"FOO": ""}, ["cmd", "--with-bar=a,b"])
        value, option = helper.handle(foo)
        self.assertEqual(NegativeOptionValue(), value)
        self.assertEqual("environment", value.origin)
        self.assertEqual("FOO=", option)

        helper = CommandLineHelper({"FOO": "a,b"}, ["cmd", "--without-bar"])
        value, option = helper.handle(foo)
        self.assertEqual(PositiveOptionValue(("a", "b")), value)
        self.assertEqual("environment", value.origin)
        self.assertEqual("FOO=a,b", option)

        helper = CommandLineHelper({}, ["cmd", "--with-foo=a,b", "FOO="])
        value, option = helper.handle(foo)
        self.assertEqual(NegativeOptionValue(), value)
        self.assertEqual("command-line", value.origin)
        self.assertEqual("FOO=", option)

        helper = CommandLineHelper({}, ["cmd", "--without-foo", "FOO=a,b"])
        value, option = helper.handle(foo)
        self.assertEqual(PositiveOptionValue(("a", "b")), value)
        self.assertEqual("command-line", value.origin)
        self.assertEqual("FOO=a,b", option)

        helper = CommandLineHelper({}, ["cmd", "FOO=", "--with-foo=a,b"])
        value, option = helper.handle(foo)
        self.assertEqual(PositiveOptionValue(("a", "b")), value)
        self.assertEqual("command-line", value.origin)
        self.assertEqual("--with-foo=a,b", option)

        helper = CommandLineHelper({}, ["cmd", "FOO=a,b", "--without-foo"])
        value, option = helper.handle(foo)
        self.assertEqual(NegativeOptionValue(), value)
        self.assertEqual("command-line", value.origin)
        self.assertEqual("--without-foo", option)

    def test_extra_args(self):
        foo = Option("--with-foo", env="FOO", nargs="*")
        helper = CommandLineHelper({}, ["cmd"])
        helper.add("FOO=a,b,c", "other-origin")
        value, option = helper.handle(foo)
        self.assertEqual(PositiveOptionValue(("a", "b", "c")), value)
        self.assertEqual("other-origin", value.origin)
        self.assertEqual("FOO=a,b,c", option)

        helper = CommandLineHelper({}, ["cmd"])
        helper.add("FOO=a,b,c", "other-origin")
        helper.add("--with-foo=a,b,c", "other-origin")
        value, option = helper.handle(foo)
        self.assertEqual(PositiveOptionValue(("a", "b", "c")), value)
        self.assertEqual("other-origin", value.origin)
        self.assertEqual("--with-foo=a,b,c", option)

        # Adding conflicting options is not allowed.
        helper = CommandLineHelper({}, ["cmd"])
        helper.add("FOO=a,b,c", "other-origin")
        with self.assertRaises(ConflictingOptionError) as cm:
            helper.add("FOO=", "other-origin")
        self.assertEqual("FOO=", cm.exception.arg)
        self.assertEqual("other-origin", cm.exception.origin)
        self.assertEqual("FOO=a,b,c", cm.exception.old_arg)
        self.assertEqual("other-origin", cm.exception.old_origin)
        with self.assertRaises(ConflictingOptionError) as cm:
            helper.add("FOO=a,b", "other-origin")
        self.assertEqual("FOO=a,b", cm.exception.arg)
        self.assertEqual("other-origin", cm.exception.origin)
        self.assertEqual("FOO=a,b,c", cm.exception.old_arg)
        self.assertEqual("other-origin", cm.exception.old_origin)
        # But adding the same is allowed.
        helper.add("FOO=a,b,c", "other-origin")
        value, option = helper.handle(foo)
        self.assertEqual(PositiveOptionValue(("a", "b", "c")), value)
        self.assertEqual("other-origin", value.origin)
        self.assertEqual("FOO=a,b,c", option)

        # The same rule as above applies when using the option form vs. the
        # variable form. But we can't detect it when .add is called.
        helper = CommandLineHelper({}, ["cmd"])
        helper.add("FOO=a,b,c", "other-origin")
        helper.add("--without-foo", "other-origin")
        with self.assertRaises(ConflictingOptionError) as cm:
            helper.handle(foo)
        self.assertEqual("--without-foo", cm.exception.arg)
        self.assertEqual("other-origin", cm.exception.origin)
        self.assertEqual("FOO=a,b,c", cm.exception.old_arg)
        self.assertEqual("other-origin", cm.exception.old_origin)
        helper = CommandLineHelper({}, ["cmd"])
        helper.add("FOO=a,b,c", "other-origin")
        helper.add("--with-foo=a,b", "other-origin")
        with self.assertRaises(ConflictingOptionError) as cm:
            helper.handle(foo)
        self.assertEqual("--with-foo=a,b", cm.exception.arg)
        self.assertEqual("other-origin", cm.exception.origin)
        self.assertEqual("FOO=a,b,c", cm.exception.old_arg)
        self.assertEqual("other-origin", cm.exception.old_origin)
        helper = CommandLineHelper({}, ["cmd"])
        helper.add("FOO=a,b,c", "other-origin")
        helper.add("--with-foo=a,b,c", "other-origin")
        value, option = helper.handle(foo)
        self.assertEqual(PositiveOptionValue(("a", "b", "c")), value)
        self.assertEqual("other-origin", value.origin)
        self.assertEqual("--with-foo=a,b,c", option)

        # Conflicts are also not allowed against what is in the
        # environment/on the command line.
        helper = CommandLineHelper({}, ["cmd", "--with-foo=a,b"])
        helper.add("FOO=a,b,c", "other-origin")
        with self.assertRaises(ConflictingOptionError) as cm:
            helper.handle(foo)
        self.assertEqual("FOO=a,b,c", cm.exception.arg)
        self.assertEqual("other-origin", cm.exception.origin)
        self.assertEqual("--with-foo=a,b", cm.exception.old_arg)
        self.assertEqual("command-line", cm.exception.old_origin)

        helper = CommandLineHelper({}, ["cmd", "--with-foo=a,b"])
        helper.add("--without-foo", "other-origin")
        with self.assertRaises(ConflictingOptionError) as cm:
            helper.handle(foo)
        self.assertEqual("--without-foo", cm.exception.arg)
        self.assertEqual("other-origin", cm.exception.origin)
        self.assertEqual("--with-foo=a,b", cm.exception.old_arg)
        self.assertEqual("command-line", cm.exception.old_origin)

    def test_possible_origins(self):
        with self.assertRaises(InvalidOptionError):
            Option("--foo", possible_origins="command-line")

        helper = CommandLineHelper({"BAZ": "1"}, ["cmd", "--foo", "--bar"])
        foo = Option("--foo", possible_origins=("command-line",))
        value, option = helper.handle(foo)
        self.assertEqual(PositiveOptionValue(), value)
        self.assertEqual("command-line", value.origin)
        self.assertEqual("--foo", option)

        bar = Option("--bar", possible_origins=("mozconfig",))
        with self.assertRaisesRegex(
            InvalidOptionError,
            "--bar can not be set by command-line. Values are accepted from: mozconfig",
        ):
            helper.handle(bar)

        baz = Option(env="BAZ", possible_origins=("implied",))
        with self.assertRaisesRegex(
            InvalidOptionError,
            "BAZ=1 can not be set by environment. Values are accepted from: implied",
        ):
            helper.handle(baz)


if __name__ == "__main__":
    main()
