import enum
import os
import re
import sys
import tempfile
from contextlib import nullcontext
from typing import Literal

if sys.version_info < (3, 11):
    enum.StrEnum = enum.Enum  # type: ignore[assignment]

import pytest

import click
from click import Option
from click import UNPROCESSED
from click._utils import UNSET
from click.testing import CliRunner


def test_prefixes(runner):
    @click.command()
    @click.option("++foo", is_flag=True, help="das foo")
    @click.option("--bar", is_flag=True, help="das bar")
    def cli(foo, bar):
        click.echo(f"foo={foo} bar={bar}")

    result = runner.invoke(cli, ["++foo", "--bar"])
    assert not result.exception
    assert result.output == "foo=True bar=True\n"

    result = runner.invoke(cli, ["--help"])
    assert re.search(r"\+\+foo\s+das foo", result.output) is not None
    assert re.search(r"--bar\s+das bar", result.output) is not None


def test_invalid_option(runner):
    with pytest.raises(TypeError, match="name was passed") as exc_info:
        click.Option(["foo"])

    message = str(exc_info.value)
    assert "name was passed (foo)" in message
    assert "declare an argument" in message
    assert "'--foo'" in message


@pytest.mark.parametrize("deprecated", [True, "USE B INSTEAD"])
def test_deprecated_usage(runner, deprecated):
    @click.command()
    @click.option("--foo", default="bar", deprecated=deprecated)
    def cmd(foo):
        click.echo(foo)

    result = runner.invoke(cmd, ["--help"])
    assert "(DEPRECATED" in result.output

    if isinstance(deprecated, str):
        assert deprecated in result.output


@pytest.mark.parametrize("deprecated", [True, "USE B INSTEAD"])
def test_deprecated_warning(runner, deprecated):
    @click.command()
    @click.option(
        "--my-option", required=False, deprecated=deprecated, default="default option"
    )
    def cli(my_option: str):
        click.echo(f"{my_option}")

    # defaults should not give a deprecated warning
    result = runner.invoke(cli, [])
    assert result.exit_code == 0, result.output
    assert "is deprecated" not in result.output

    result = runner.invoke(cli, ["--my-option", "hello"])
    assert result.exit_code == 0, result.output
    assert "option 'my_option' is deprecated" in result.output

    if isinstance(deprecated, str):
        assert deprecated in result.output


def test_deprecated_required(runner):
    with pytest.raises(ValueError, match="is deprecated and still required"):
        click.Option(["--a"], required=True, deprecated=True)


def test_deprecated_prompt(runner):
    with pytest.raises(ValueError, match="`deprecated` options cannot use `prompt`"):
        click.Option(["--a"], prompt=True, deprecated=True)


def test_invalid_nargs(runner):
    with pytest.raises(TypeError, match="nargs=-1 is not supported for options."):

        @click.command()
        @click.option("--foo", nargs=-1)
        def cli(foo):
            pass


def test_nargs_tup_composite_mult(runner):
    @click.command()
    @click.option("--item", type=(str, int), multiple=True)
    def copy(item):
        for name, id in item:
            click.echo(f"name={name} id={id:d}")

    result = runner.invoke(copy, ["--item", "peter", "1", "--item", "max", "2"])
    assert not result.exception
    assert result.output.splitlines() == ["name=peter id=1", "name=max id=2"]


def test_counting(runner):
    @click.command()
    @click.option("-v", count=True, help="Verbosity", type=click.IntRange(0, 3))
    def cli(v):
        click.echo(f"verbosity={v:d}")

    result = runner.invoke(cli, ["-vvv"])
    assert not result.exception
    assert result.output == "verbosity=3\n"

    result = runner.invoke(cli, ["-vvvv"])
    assert result.exception
    assert "Invalid value for '-v': 4 is not in the range 0<=x<=3." in result.output

    result = runner.invoke(cli, [])
    assert not result.exception
    assert result.output == "verbosity=0\n"

    result = runner.invoke(cli, ["--help"])
    assert re.search(r"-v\s+Verbosity", result.output) is not None


@pytest.mark.parametrize("unknown_flag", ["--foo", "-f"])
def test_unknown_options(runner, unknown_flag):
    @click.command()
    def cli():
        pass

    result = runner.invoke(cli, [unknown_flag])
    assert result.exception
    assert f"No such option '{unknown_flag}'." in result.output


@pytest.mark.parametrize(
    ("value", "expect"),
    [
        ("--cat", "Did you mean '--count'?"),
        ("--bounds", "(Did you mean one of: '--bound', '--count'?)"),
        ("--bount", "(Did you mean one of: '--bound', '--count'?)"),
    ],
)
def test_suggest_possible_options(runner, value, expect):
    cli = click.Command(
        "cli", params=[click.Option(["--bound"]), click.Option(["--count"])]
    )
    result = runner.invoke(cli, [value])
    assert expect in result.output


def test_multiple_required(runner):
    @click.command()
    @click.option("-m", "--message", multiple=True, required=True)
    def cli(message):
        click.echo("\n".join(message))

    result = runner.invoke(cli, ["-m", "foo", "-mbar"])
    assert not result.exception
    assert result.output == "foo\nbar\n"

    result = runner.invoke(cli, [])
    assert result.exception
    assert "Error: Missing option '-m' / '--message'." in result.output


@pytest.mark.parametrize(
    ("multiple", "nargs", "default", "expected"),
    [
        # If multiple values are allowed, defaults should be iterable.
        (True, 1, [], ()),
        (True, 1, (), ()),
        (True, 1, tuple(), ()),
        (True, 1, set(), ()),
        (True, 1, frozenset(), ()),
        (True, 1, {}, ()),
        # Special values.
        (True, 1, None, ()),
        (True, 1, UNSET, ()),
        # Number of values are kept as-is in the default.
        (True, 1, [1], (1,)),
        (True, 1, [1, 2], (1, 2)),
        (True, 1, [1, 2, 3], (1, 2, 3)),
        (True, 1, [1.1, 2.2], (1.1, 2.2)),
        (True, 1, ["1", "2"], ("1", "2")),
        (True, 1, [None, None], (None, None)),
        # Contrary to list or tuples, native Python types not supported by Click are
        # not recognized and are converted to the default format: tuple of strings.
        # Refs: https://github.com/pallets/click/issues/3036
        (True, 1, {1, 2}, ("1", "2")),
        (True, 1, frozenset([1, 2]), ("1", "2")),
        (True, 1, {1: "a", 2: "b"}, ("1", "2")),
        # Multiple values with nargs > 1.
        (True, 2, [], ()),
        (True, 2, (), ()),
        (True, 2, tuple(), ()),
        (True, 2, set(), ()),
        (True, 2, frozenset(), ()),
        (True, 2, {}, ()),
        (True, 2, None, ()),
        (True, 2, UNSET, ()),
        (True, 2, [[1, 2]], ((1, 2),)),
        (True, 2, [[1, 2], [3, 4]], ((1, 2), (3, 4))),
        (True, 2, [[1, 2], [3, 4], [5, 6]], ((1, 2), (3, 4), (5, 6))),
        (True, 2, [[1.1, 2.2], [3.3, 4.4]], ((1.1, 2.2), (3.3, 4.4))),
        (True, 2, [["1", "2"], ["3", "4"]], (("1", "2"), ("3", "4"))),
        (True, 2, [[None, None], [None, None]], ((None, None), (None, None))),
        (True, 2, [[1, 2.2], ["3", None]], ((1, 2.2), (3, None))),
        (True, 2, [[1, 2.2], None], ((1, 2.2), None)),
        # Default of the right length works for non-multiples.
        (False, 2, [1, 2], (1, 2)),
    ],
)
def test_good_defaults_for_multiple(runner, multiple, nargs, default, expected):
    """Comprehensive check of default-value processing for options with
    ``multiple=True`` and/or ``nargs > 1``.

    .. hint::
        An argument-specific equivalent is in
        ``test_arguments.py::test_good_defaults_for_nargs``.

        Smoke tests are in ``test_defaults.py``:
        ``test_multiple_defaults`` (explicit ``type=FLOAT``)
        and ``test_nargs_plus_multiple`` (``nargs=2``).
    """

    @click.command()
    @click.option("-a", multiple=multiple, nargs=nargs, default=default)
    def cmd(a):
        click.echo(repr(a), nl=False)

    result = runner.invoke(cmd)
    assert result.output == repr(expected)


@pytest.mark.parametrize(
    ("multiple", "nargs", "default", "exception", "message"),
    [
        # Non-iterables defaults.
        (
            True,
            1,
            "Yo",
            None,
            "Error: Invalid value for '-a': Value must be an iterable.",
        ),
        (
            True,
            1,
            "",
            None,
            "Error: Invalid value for '-a': Value must be an iterable.",
        ),
        (
            True,
            1,
            True,
            None,
            "Error: Invalid value for '-a': Value must be an iterable.",
        ),
        (
            True,
            1,
            False,
            None,
            "Error: Invalid value for '-a': Value must be an iterable.",
        ),
        (
            True,
            1,
            12,
            None,
            "Error: Invalid value for '-a': Value must be an iterable.",
        ),
        (
            True,
            1,
            7.9,
            None,
            "Error: Invalid value for '-a': Value must be an iterable.",
        ),
        (
            False,
            2,
            42,
            None,
            "Error: Invalid value for '-a': Value must be an iterable.",
        ),
        (
            True,
            2,
            ["test string which is not a list in the list"],
            None,
            "Error: Invalid value for '-a': Value must be an iterable.",
        ),
        # Multiple options, each with 2 args, but with wrong length.
        (
            True,
            2,
            (1,),
            None,
            "Error: Invalid value for '-a': Value must be an iterable.",
        ),
        (
            True,
            2,
            (1, 2, 3),
            None,
            "Error: Invalid value for '-a': Value must be an iterable.",
        ),
        (
            True,
            2,
            [tuple()],
            ValueError,
            r"'nargs' must be 0 \(or None\) for type <click\.types\.Tuple object at "
            r"0x[0-9A-Fa-f]+>, but it was 2\.",
        ),
        (
            True,
            2,
            [(1,)],
            ValueError,
            r"'nargs' must be 1 \(or None\) for type <click\.types\.Tuple object at "
            r"0x[0-9A-Fa-f]+>, but it was 2\.",
        ),
        (
            True,
            2,
            [(1, 2, 3)],
            ValueError,
            r"'nargs' must be 3 \(or None\) for type <click\.types\.Tuple object at "
            r"0x[0-9A-Fa-f]+>, but it was 2\.",
        ),
        # A mix of valid and invalid defaults.
        (
            True,
            2,
            [[1, 2.2], []],
            None,
            "Error: Invalid value for '-a': 2 values are required, but 0 were given.",
        ),
        # Default values that are iterable but not of the right length.
        (
            False,
            2,
            [1],
            None,
            "Error: Invalid value for '-a': Takes 2 values but 1 was given.",
        ),
        (
            True,
            2,
            [[1]],
            ValueError,
            r"'nargs' must be 1 \(or None\) for type <click\.types\.Tuple object at "
            r"0x[0-9A-Fa-f]+>, but it was 2\.",
        ),
    ],
)
def test_bad_defaults_for_multiple(
    runner, multiple, nargs, default, exception, message
):
    if exception:
        assert issubclass(exception, Exception)
    else:
        assert exception is None

    with (
        pytest.raises(exception, match=re.compile(message))
        if exception
        else nullcontext()
    ):

        @click.command()
        @click.option("-a", multiple=multiple, nargs=nargs, default=default)
        def cmd(a):
            click.echo(repr(a))

        result = runner.invoke(cmd)
        assert message in result.stderr


@pytest.mark.parametrize("env_key", ["MYPATH", "AUTO_MYPATH"])
def test_empty_envvar(runner, env_key):
    @click.command()
    @click.option("--mypath", type=click.Path(exists=True), envvar="MYPATH")
    def cli(mypath):
        click.echo(f"mypath: {mypath}")

    result = runner.invoke(cli, env={env_key: ""}, auto_envvar_prefix="AUTO")
    assert result.exception is None
    assert result.output == "mypath: None\n"


def test_multiple_envvar(runner):
    @click.command()
    @click.option("--arg", multiple=True)
    def cmd(arg):
        click.echo("|".join(arg))

    result = runner.invoke(
        cmd, [], auto_envvar_prefix="TEST", env={"TEST_ARG": "foo bar baz"}
    )
    assert not result.exception
    assert result.output == "foo|bar|baz\n"

    @click.command()
    @click.option("--arg", multiple=True, envvar="X")
    def cmd(arg):
        click.echo("|".join(arg))

    result = runner.invoke(cmd, [], env={"X": "foo bar baz"})
    assert not result.exception
    assert result.output == "foo|bar|baz\n"

    @click.command()
    @click.option("--arg", multiple=True, type=click.Path())
    def cmd(arg):
        click.echo("|".join(arg))

    result = runner.invoke(
        cmd,
        [],
        auto_envvar_prefix="TEST",
        env={"TEST_ARG": f"foo{os.path.pathsep}bar"},
    )
    assert not result.exception
    assert result.output == "foo|bar\n"


@pytest.mark.parametrize(
    ("envvar_name", "envvar_value", "expected"),
    (
        # Lower-cased variations of value.
        ("SHOUT", "true", True),
        ("SHOUT", "false", False),
        # Title-cased variations of value.
        ("SHOUT", "True", True),
        ("SHOUT", "False", False),
        # Upper-cased variations of value.
        ("SHOUT", "TRUE", True),
        ("SHOUT", "FALSE", False),
        # Random-cased variations of value.
        ("SHOUT", "TruE", True),
        ("SHOUT", "truE", True),
        ("SHOUT", "FaLsE", False),
        ("SHOUT", "falsE", False),
        # Extra spaces around the value.
        ("SHOUT", "true    ", True),
        ("SHOUT", "  true  ", True),
        ("SHOUT", "    true", True),
        ("SHOUT", "false   ", False),
        ("SHOUT", "  false ", False),
        ("SHOUT", "   false", False),
        # Integer variations.
        ("SHOUT", "1", True),
        ("SHOUT", "0", False),
        # Alternative states.
        ("SHOUT", "t", True),
        ("SHOUT", "T", True),
        ("SHOUT", "  T  ", True),
        ("SHOUT", "f", False),
        ("SHOUT", "y", True),
        ("SHOUT", "n", False),
        ("SHOUT", "yes", True),
        ("SHOUT", "no", False),
        ("SHOUT", "on", True),
        ("SHOUT", "off", False),
        # Blank value variations.
        ("SHOUT", None, False),
        ("SHOUT", "", False),
        ("SHOUT", " ", False),
        ("SHOUT", "       ", False),
        # Variable names are not stripped of spaces and so don't match the
        # flag, which then naturraly takes its default value.
        ("SHOUT    ", "True", False),
        ("SHOUT    ", "False", False),
        ("  SHOUT  ", "True", False),
        ("  SHOUT  ", "False", False),
        ("    SHOUT", "True", False),
        ("    SHOUT", "False", False),
        ("SH    OUT", "True", False),
        ("SH    OUT", "False", False),
        # Same for random and reverse environment variable names: they are not
        # recognized by the option.
        ("RANDOM", "True", False),
        ("NO_SHOUT", "True", False),
        ("NO_SHOUT", "False", False),
        ("NOSHOUT", "True", False),
        ("NOSHOUT", "False", False),
    ),
)
def test_boolean_flag_envvar(runner, envvar_name, envvar_value, expected):
    assert isinstance(envvar_name, str)
    assert isinstance(envvar_value, str) or envvar_value is None

    @click.command()
    @click.option("--shout/--no-shout", envvar="SHOUT")
    def cli(shout):
        click.echo(repr(shout), nl=False)

    result = runner.invoke(cli, [], env={envvar_name: envvar_value})
    assert result.exit_code == 0
    assert result.output == repr(expected)


@pytest.mark.parametrize(
    "value",
    (
        # Extra spaces inside the value.
        "tr ue",  # codespell:ignore ue
        "fa lse",
        # Numbers.
        "10",
        "01",
        "00",
        "11",
        "0.0",
        "1.1",
        # Random strings.
        "randomstring",
        "None",
        "'None'",
        "A B",
        " 1 2 ",
        "9.3",
        "a;n",
        "x:y",
        "i/o",
    ),
)
def test_boolean_envvar_bad_values(runner, value):
    @click.command()
    @click.option("--shout/--no-shout", envvar="SHOUT")
    def cli(shout):
        click.echo(shout)

    result = runner.invoke(cli, [], env={"SHOUT": value})
    assert result.exit_code == 2
    assert (
        f"Invalid value for '--shout': {value!r} is not a valid boolean."
        in result.output
    )


def test_multiple_default_help(runner):
    @click.command()
    @click.option("--arg1", multiple=True, default=("foo", "bar"), show_default=True)
    @click.option("--arg2", multiple=True, default=(1, 2), type=int, show_default=True)
    def cmd(arg, arg2):
        pass

    result = runner.invoke(cmd, ["--help"])
    assert not result.exception
    assert "foo, bar" in result.output
    assert "1, 2" in result.output


def test_show_default_default_map(runner):
    @click.command()
    @click.option("--arg", default="a", show_default=True)
    def cmd(arg):
        click.echo(arg)

    result = runner.invoke(cmd, ["--help"], default_map={"arg": "b"})

    assert not result.exception
    assert "[default: b]" in result.output


def test_multiple_default_type():
    opt = click.Option(["-a"], multiple=True, default=(1, 2))
    assert opt.nargs == 1
    assert opt.multiple
    assert opt.type is click.INT
    ctx = click.Context(click.Command("test"))
    assert opt.get_default(ctx) == (1, 2)


def test_multiple_default_composite_type():
    opt = click.Option(["-a"], multiple=True, default=[(1, "a")])
    assert opt.nargs == 2
    assert opt.multiple
    assert isinstance(opt.type, click.Tuple)
    assert opt.type.types == [click.INT, click.STRING]
    ctx = click.Context(click.Command("test"))
    assert opt.type_cast_value(ctx, opt.get_default(ctx)) == ((1, "a"),)


def test_parse_multiple_default_composite_type(runner):
    @click.command()
    @click.option("-a", multiple=True, default=("a", "b"))
    @click.option("-b", multiple=True, default=[(1, "a")])
    def cmd(a, b):
        click.echo(a)
        click.echo(b)

    # result = runner.invoke(cmd, "-a c -a 1 -a d -b 2 two -b 4 four".split())
    # assert result.output == "('c', '1', 'd')\n((2, 'two'), (4, 'four'))\n"
    result = runner.invoke(cmd)
    assert result.output == "('a', 'b')\n((1, 'a'),)\n"


def test_dynamic_default_help_unset(runner):
    @click.command()
    @click.option(
        "--username",
        prompt=True,
        default=lambda: os.environ.get("USER", ""),
        show_default=True,
    )
    def cmd(username):
        print("Hello,", username)

    result = runner.invoke(cmd, ["--help"])
    assert result.exit_code == 0
    assert "--username" in result.output
    assert "lambda" not in result.output
    assert "(dynamic)" in result.output


def test_dynamic_default_help_text(runner):
    @click.command()
    @click.option(
        "--username",
        prompt=True,
        default=lambda: os.environ.get("USER", ""),
        show_default="current user",
    )
    def cmd(username):
        print("Hello,", username)

    result = runner.invoke(cmd, ["--help"])
    assert result.exit_code == 0
    assert "--username" in result.output
    assert "lambda" not in result.output
    assert "(current user)" in result.output


def test_dynamic_default_help_special_method(runner):
    class Value:
        def __call__(self):
            return 42

        def __str__(self):
            return "special value"

    opt = click.Option(["-a"], default=Value(), show_default=True)
    ctx = click.Context(click.Command("cli"))
    assert opt.get_help_extra(ctx) == {"default": "special value"}
    assert "special value" in opt.get_help_record(ctx)[1]


@pytest.mark.parametrize(
    ("type", "expect"),
    [
        (click.IntRange(1, 32), "1<=x<=32"),
        (click.IntRange(1, 32, min_open=True, max_open=True), "1<x<32"),
        (click.IntRange(1), "x>=1"),
        (click.IntRange(max=32), "x<=32"),
    ],
)
def test_intrange_default_help_text(type, expect):
    option = click.Option(["--num"], type=type, show_default=True, default=2)
    context = click.Context(click.Command("test"))
    assert option.get_help_extra(context) == {"default": "2", "range": expect}
    result = option.get_help_record(context)[1]
    assert expect in result


def test_count_default_type_help():
    """A count option with the default type should not show >=0 in help."""
    option = click.Option(["--count"], count=True, help="some words")
    context = click.Context(click.Command("test"))
    assert option.get_help_extra(context) == {}
    result = option.get_help_record(context)[1]
    assert result == "some words"


def test_file_type_help_default():
    """The default for a File type is a filename string. The string
    should be displayed in help, not an open file object.

    Type casting is only applied to defaults in processing, not when
    getting the default value.
    """
    option = click.Option(
        ["--in"], type=click.File(), default=__file__, show_default=True
    )
    context = click.Context(click.Command("test"))
    assert option.get_help_extra(context) == {"default": __file__}
    result = option.get_help_record(context)[1]
    assert __file__ in result


def test_toupper_envvar_prefix(runner):
    @click.command()
    @click.option("--arg")
    def cmd(arg):
        click.echo(arg)

    result = runner.invoke(cmd, [], auto_envvar_prefix="test", env={"TEST_ARG": "foo"})
    assert not result.exception
    assert result.output == "foo\n"


def test_nargs_envvar(runner):
    @click.command()
    @click.option("--arg", nargs=2)
    def cmd(arg):
        click.echo("|".join(arg))

    result = runner.invoke(
        cmd, [], auto_envvar_prefix="TEST", env={"TEST_ARG": "foo bar"}
    )
    assert not result.exception
    assert result.output == "foo|bar\n"

    @click.command()
    @click.option("--arg", nargs=2, multiple=True)
    def cmd(arg):
        for item in arg:
            click.echo("|".join(item))

    result = runner.invoke(
        cmd, [], auto_envvar_prefix="TEST", env={"TEST_ARG": "x 1 y 2"}
    )
    assert not result.exception
    assert result.output == "x|1\ny|2\n"


def test_show_envvar(runner):
    @click.command()
    @click.option("--arg1", envvar="ARG1", show_envvar=True)
    def cmd(arg):
        pass

    result = runner.invoke(cmd, ["--help"])
    assert not result.exception
    assert "ARG1" in result.output


def test_show_envvar_auto_prefix(runner):
    @click.command()
    @click.option("--arg1", show_envvar=True)
    def cmd(arg):
        pass

    result = runner.invoke(cmd, ["--help"], auto_envvar_prefix="TEST")
    assert not result.exception
    assert "TEST_ARG1" in result.output


def test_show_envvar_auto_prefix_dash_in_command(runner):
    @click.group()
    def cli():
        pass

    @cli.command()
    @click.option("--baz", show_envvar=True)
    def foo_bar(baz):
        pass

    result = runner.invoke(cli, ["foo-bar", "--help"], auto_envvar_prefix="TEST")
    assert not result.exception
    assert "TEST_FOO_BAR_BAZ" in result.output


def test_custom_validation(runner):
    def validate_pos_int(ctx, param, value):
        if value < 0:
            raise click.BadParameter("Value needs to be positive")
        return value

    @click.command()
    @click.option("--foo", callback=validate_pos_int, default=1)
    def cmd(foo):
        click.echo(foo)

    result = runner.invoke(cmd, ["--foo", "-1"])
    assert "Invalid value for '--foo': Value needs to be positive" in result.output

    result = runner.invoke(cmd, ["--foo", "42"])
    assert result.output == "42\n"


def test_callback_validates_prompt(runner, monkeypatch):
    def validate(ctx, param, value):
        if value < 0:
            raise click.BadParameter("should be positive")

        return value

    @click.command()
    @click.option("-a", type=int, callback=validate, prompt=True)
    def cli(a):
        click.echo(a)

    result = runner.invoke(cli, input="-12\n60\n")
    assert result.output == "A: -12\nError: should be positive\nA: 60\n60\n"


def test_winstyle_options(runner):
    @click.command()
    @click.option("/debug;/no-debug", help="Enables or disables debug mode.")
    def cmd(debug):
        click.echo(debug)

    result = runner.invoke(cmd, ["/debug"], help_option_names=["/?"])
    assert result.output == "True\n"
    result = runner.invoke(cmd, ["/no-debug"], help_option_names=["/?"])
    assert result.output == "False\n"
    result = runner.invoke(cmd, [], help_option_names=["/?"])
    assert result.output == "False\n"
    result = runner.invoke(cmd, ["/?"], help_option_names=["/?"])
    assert "/debug; /no-debug  Enables or disables debug mode." in result.output
    assert "/?                 Show this message and exit." in result.output


def test_legacy_options(runner):
    @click.command()
    @click.option("-whatever")
    def cmd(whatever):
        click.echo(whatever)

    result = runner.invoke(cmd, ["-whatever", "42"])
    assert result.output == "42\n"
    result = runner.invoke(cmd, ["-whatever=23"])
    assert result.output == "23\n"


@pytest.mark.parametrize(
    ("value", "expect_missing", "processed_value"),
    [
        (UNSET, True, None),
        (None, False, None),
        # Default type of the argument is str, so everything is processed as strings.
        ("", False, ""),
        ("  ", False, "  "),
        ("foo", False, "foo"),
        ("12", False, "12"),
        (12, False, "12"),
        (12.1, False, "12.1"),
        (list(), False, "[]"),
        (tuple(), False, "()"),
        (set(), False, "set()"),
        (frozenset(), False, "frozenset()"),
        (dict(), False, "{}"),
    ],
)
def test_required_option(value, expect_missing, processed_value):
    ctx = click.Context(click.Command(""))
    argument = click.Option(["-a"], required=True)

    if expect_missing:
        with pytest.raises(click.MissingParameter) as excinfo:
            argument.process_value(ctx, value)
        assert str(excinfo.value) == "Missing parameter: a"

    else:
        value = argument.process_value(ctx, value)
        assert value == processed_value


def test_missing_required_flag(runner):
    cli = click.Command(
        "cli", params=[click.Option(["--on/--off"], is_flag=True, required=True)]
    )
    result = runner.invoke(cli)
    assert result.exit_code == 2
    assert "Error: Missing option '--on'." in result.output


def test_missing_choice(runner):
    @click.command()
    @click.option("--foo", type=click.Choice(["foo", "bar"]), required=True)
    def cmd(foo):
        click.echo(foo)

    result = runner.invoke(cmd)
    assert result.exit_code == 2
    error, separator, choices = result.output.partition("Choose from")
    assert "Error: Missing option '--foo'. " in error
    assert "Choose from" in separator
    assert "foo" in choices
    assert "bar" in choices


def test_missing_envvar(runner):
    cli = click.Command(
        "cli", params=[click.Option(["--foo"], envvar="bar", required=True)]
    )
    result = runner.invoke(cli)
    assert result.exit_code == 2
    assert "Error: Missing option '--foo'." in result.output
    cli = click.Command(
        "cli",
        params=[click.Option(["--foo"], envvar="bar", show_envvar=True, required=True)],
    )
    result = runner.invoke(cli)
    assert result.exit_code == 2
    assert "Error: Missing option '--foo' (env var: 'bar')." in result.output

    cli = click.Command(
        "cli", params=[click.Option(["--foo"], show_envvar=True, required=True)]
    )
    result = runner.invoke(cli)
    assert result.exit_code == 2
    assert "Error: Missing option '--foo'." in result.output


def test_case_insensitive_choice(runner):
    @click.command()
    @click.option("--foo", type=click.Choice(["Orange", "Apple"], case_sensitive=False))
    def cmd(foo):
        click.echo(foo)

    result = runner.invoke(cmd, ["--foo", "apple"])
    assert result.exit_code == 0
    assert result.output == "Apple\n"

    result = runner.invoke(cmd, ["--foo", "oRANGe"])
    assert result.exit_code == 0
    assert result.output == "Orange\n"

    result = runner.invoke(cmd, ["--foo", "Apple"])
    assert result.exit_code == 0
    assert result.output == "Apple\n"

    @click.command()
    @click.option("--foo", type=click.Choice(["Orange", "Apple"]))
    def cmd2(foo):
        click.echo(foo)

    result = runner.invoke(cmd2, ["--foo", "apple"])
    assert result.exit_code == 2

    result = runner.invoke(cmd2, ["--foo", "oRANGe"])
    assert result.exit_code == 2

    result = runner.invoke(cmd2, ["--foo", "Apple"])
    assert result.exit_code == 0


def test_case_insensitive_choice_returned_exactly(runner):
    @click.command()
    @click.option("--foo", type=click.Choice(["Orange", "Apple"], case_sensitive=False))
    def cmd(foo):
        click.echo(foo)

    result = runner.invoke(cmd, ["--foo", "apple"])
    assert result.exit_code == 0
    assert result.output == "Apple\n"


def test_option_help_preserve_paragraphs(runner):
    @click.command()
    @click.option(
        "-C",
        "--config",
        type=click.Path(),
        help="""Configuration file to use.

        If not given, the environment variable CONFIG_FILE is consulted
        and used if set. If neither are given, a default configuration
        file is loaded.""",
    )
    def cmd(config):
        pass

    result = runner.invoke(cmd, ["--help"])
    assert result.exit_code == 0
    i = " " * 21
    assert (
        "  -C, --config PATH  Configuration file to use.\n"
        f"{i}\n"
        f"{i}If not given, the environment variable CONFIG_FILE is\n"
        f"{i}consulted and used if set. If neither are given, a default\n"
        f"{i}configuration file is loaded."
    ) in result.output


def test_argument_custom_class(runner):
    class CustomArgument(click.Argument):
        def get_default(self, ctx, call=True):
            """a dumb override of a default value for testing"""
            return "I am a default"

    @click.command()
    @click.argument("testarg", cls=CustomArgument, default="you won't see me")
    def cmd(testarg):
        click.echo(testarg)

    result = runner.invoke(cmd)
    assert "I am a default" in result.output
    assert "you won't see me" not in result.output


def test_option_custom_class(runner):
    class CustomOption(click.Option):
        def get_help_record(self, ctx):
            """a dumb override of a help text for testing"""
            return ("--help", "I am a help text")

    @click.command()
    @click.option("--testoption", cls=CustomOption, help="you won't see me")
    def cmd(testoption):
        click.echo(testoption)

    result = runner.invoke(cmd, ["--help"])
    assert "I am a help text" in result.output
    assert "you won't see me" not in result.output


@pytest.mark.parametrize(
    ("param_decl", "option_kwargs", "pass_argv"),
    (
        # there is a large potential parameter space to explore here
        # this is just a very small sample of it
        ("--opt", {}, []),
        ("--opt", {"multiple": True}, []),
        ("--opt", {"is_flag": True}, []),
        ("--opt/--no-opt", {"is_flag": True, "default": None}, []),
        ("--req", {"is_flag": True, "required": True}, ["--req"]),
    ),
)
def test_option_custom_class_can_override_type_cast_value_and_never_sees_unset(
    runner, param_decl, option_kwargs, pass_argv
):
    """
    Test that overriding type_cast_value is supported

    In particular, the option is never passed an UNSET sentinel value.
    """

    class CustomOption(click.Option):
        def type_cast_value(self, ctx, value):
            assert value is not UNSET
            return value

    @click.command()
    @click.option("myparam", param_decl, **option_kwargs, cls=CustomOption)
    def cmd(myparam):
        click.echo("ok")

    result = runner.invoke(cmd, pass_argv)
    assert not result.exception
    assert result.exit_code == 0


def test_option_custom_class_reusable(runner):
    """Ensure we can reuse a custom class option. See Issue #926"""

    class CustomOption(click.Option):
        def get_help_record(self, ctx):
            """a dumb override of a help text for testing"""
            return ("--help", "I am a help text")

    # Assign to a variable to reuse the decorator.
    testoption = click.option("--testoption", cls=CustomOption, help="you won't see me")

    @click.command()
    @testoption
    def cmd1(testoption):
        click.echo(testoption)

    @click.command()
    @testoption
    def cmd2(testoption):
        click.echo(testoption)

    # Both of the commands should have the --help option now.
    for cmd in (cmd1, cmd2):
        result = runner.invoke(cmd, ["--help"])
        assert "I am a help text" in result.output
        assert "you won't see me" not in result.output


@pytest.mark.parametrize("custom_class", (True, False))
@pytest.mark.parametrize(
    ("name_specs", "expected"),
    (
        (
            ("-h", "--help"),
            "  -h, --help  Show this message and exit.\n",
        ),
        (
            ("-h",),
            "  -h      Show this message and exit.\n"
            "  --help  Show this message and exit.\n",
        ),
        (
            ("--help",),
            "  --help  Show this message and exit.\n",
        ),
    ),
)
def test_help_option_custom_names_and_class(runner, custom_class, name_specs, expected):
    class CustomHelpOption(click.Option):
        pass

    option_attrs = {}
    if custom_class:
        option_attrs["cls"] = CustomHelpOption

    @click.command()
    @click.help_option(*name_specs, **option_attrs)
    def cmd():
        pass

    for arg in name_specs:
        result = runner.invoke(cmd, [arg])
        assert not result.exception
        assert result.exit_code == 0
        assert expected in result.output


def test_bool_flag_with_type(runner):
    @click.command()
    @click.option("--shout/--no-shout", default=False, type=bool)
    def cmd(shout):
        pass

    result = runner.invoke(cmd)
    assert not result.exception


def test_aliases_for_flags(runner):
    @click.command()
    @click.option("--warnings/--no-warnings", " /-W", default=True)
    def cli(warnings):
        click.echo(warnings)

    result = runner.invoke(cli, ["--warnings"])
    assert result.output == "True\n"
    result = runner.invoke(cli, ["--no-warnings"])
    assert result.output == "False\n"
    result = runner.invoke(cli, ["-W"])
    assert result.output == "False\n"

    @click.command()
    @click.option("--warnings/--no-warnings", "-w", default=True)
    def cli_alt(warnings):
        click.echo(warnings)

    result = runner.invoke(cli_alt, ["--warnings"])
    assert result.output == "True\n"
    result = runner.invoke(cli_alt, ["--no-warnings"])
    assert result.output == "False\n"
    result = runner.invoke(cli_alt, ["-w"])
    assert result.output == "True\n"


@pytest.mark.parametrize(
    "option_args,expected",
    [
        (["--aggressive", "--all", "-a"], "aggressive"),
        (["--first", "--second", "--third", "-a", "-b", "-c"], "first"),
        (["--apple", "--banana", "--cantaloupe", "-a", "-b", "-c"], "apple"),
        (["--cantaloupe", "--banana", "--apple", "-c", "-b", "-a"], "cantaloupe"),
        (["-a", "-b", "-c"], "a"),
        (["-c", "-b", "-a"], "c"),
        (["-a", "--apple", "-b", "--banana", "-c", "--cantaloupe"], "apple"),
        (["-c", "-a", "--cantaloupe", "-b", "--banana", "--apple"], "cantaloupe"),
        (["--from", "-f", "_from"], "_from"),
        (["--return", "-r", "_ret"], "_ret"),
    ],
)
def test_option_names(runner, option_args, expected):
    @click.command()
    @click.option(*option_args, is_flag=True)
    def cmd(**kwargs):
        click.echo(str(kwargs[expected]))

    assert cmd.params[0].name == expected

    for form in option_args:
        if form.startswith("-"):
            result = runner.invoke(cmd, [form])
            assert result.output == "True\n"


def test_flag_duplicate_names(runner):
    with pytest.raises(ValueError, match="cannot use the same flag for true/false"):
        click.Option(["--foo/--foo"], default=False)


@pytest.mark.parametrize(("default", "expect"), [(False, "no-cache"), (True, "cache")])
def test_show_default_boolean_flag_name(runner, default, expect):
    """When a boolean flag has distinct True/False opts, it should show
    the default opt name instead of the default value. It should only
    show one name even if multiple are declared.
    """
    opt = click.Option(
        ("--cache/--no-cache", "--c/--nc"),
        default=default,
        show_default=True,
        help="Enable/Disable the cache.",
    )
    ctx = click.Context(click.Command("test"))
    assert opt.get_help_extra(ctx) == {"default": expect}
    message = opt.get_help_record(ctx)[1]
    assert f"[default: {expect}]" in message


def test_show_true_default_boolean_flag_value(runner):
    """When a boolean flag only has one opt and its default is True,
    it will show the default value, not the opt name.
    """
    opt = click.Option(
        ("--cache",),
        is_flag=True,
        show_default=True,
        default=True,
        help="Enable the cache.",
    )
    ctx = click.Context(click.Command("test"))
    assert opt.get_help_extra(ctx) == {"default": "True"}
    message = opt.get_help_record(ctx)[1]
    assert "[default: True]" in message


@pytest.mark.parametrize("default", [False, None])
def test_hide_false_default_boolean_flag_value(runner, default):
    """When a boolean flag only has one opt and its default is False or
    None, it will not show the default
    """
    opt = click.Option(
        ("--cache",),
        is_flag=True,
        show_default=True,
        default=default,
        help="Enable the cache.",
    )
    ctx = click.Context(click.Command("test"))
    assert opt.get_help_extra(ctx) == {}
    message = opt.get_help_record(ctx)[1]
    assert "[default: " not in message


def test_show_default_string(runner):
    """When show_default is a string show that value as default."""
    opt = click.Option(["--limit"], show_default="unlimited")
    ctx = click.Context(click.Command("cli"))
    assert opt.get_help_extra(ctx) == {"default": "(unlimited)"}
    message = opt.get_help_record(ctx)[1]
    assert "[default: (unlimited)]" in message


def test_string_show_default_shows_custom_string_in_prompt(runner):
    @click.command()
    @click.option(
        "--arg1", show_default="custom", prompt=True, default="my-default-value"
    )
    def cmd(arg1):
        pass

    result = runner.invoke(cmd, input="my-input", standalone_mode=False)
    assert "(custom)" in result.output
    assert "my-default-value" not in result.output


class _StrictEq:
    """Object whose ``__eq__`` raises on string comparison (like semver.Version)."""

    def __eq__(self, other):
        if isinstance(other, str):
            raise ValueError("cannot compare to string")
        return NotImplemented

    def __str__(self):
        return "strict"


@pytest.mark.parametrize(
    ("default", "expected"),
    [
        ("", '[default: ""]'),
        (_StrictEq(), "[default: strict]"),
    ],
    ids=["empty-string", "non-string-comparable-object"],
)
def test_show_default_with_empty_string(runner, default, expected):
    """The empty-string check in help rendering must not break on objects
    whose ``__eq__`` raises for string operands.

    Regression test for https://github.com/pallets/click/issues/3298.
    """
    opt = click.Option(["--limit"], default=default, show_default=True)
    ctx = click.Context(click.Command("cli"))
    message = opt.get_help_record(ctx)[1]
    assert expected in message


def test_do_not_show_no_default(runner):
    """When show_default is True and no default is set do not show None."""
    opt = click.Option(["--limit"], show_default=True)
    ctx = click.Context(click.Command("cli"))
    assert opt.get_help_extra(ctx) == {}
    message = opt.get_help_record(ctx)[1]
    assert "[default: None]" not in message


def test_do_not_show_default_empty_multiple():
    """When show_default is True and multiple=True is set, it should not
    print empty default value in --help output.
    """
    opt = click.Option(["-a"], multiple=True, help="values", show_default=True)
    ctx = click.Context(click.Command("cli"))
    assert opt.get_help_extra(ctx) == {}
    message = opt.get_help_record(ctx)[1]
    assert message == "values"


@pytest.mark.parametrize(
    ("ctx_value", "opt_value", "extra_value", "expect"),
    [
        (None, None, {}, False),
        (None, False, {}, False),
        (None, True, {"default": "1"}, True),
        (False, None, {}, False),
        (False, False, {}, False),
        (False, True, {"default": "1"}, True),
        (True, None, {"default": "1"}, True),
        (True, False, {}, False),
        (True, True, {"default": "1"}, True),
        (False, "one", {"default": "(one)"}, True),
    ],
)
def test_show_default_precedence(ctx_value, opt_value, extra_value, expect):
    ctx = click.Context(click.Command("test"), show_default=ctx_value)
    opt = click.Option("-a", default=1, help="value", show_default=opt_value)
    assert opt.get_help_extra(ctx) == extra_value
    help = opt.get_help_record(ctx)[1]
    assert ("default:" in help) is expect


@pytest.mark.parametrize(
    ("args", "expect"),
    [
        (None, (None, None, ())),
        (["--opt"], ("flag", None, ())),
        (["--opt", "-a", 42], ("flag", "42", ())),
        (["--opt", "test", "-a", 42], ("test", "42", ())),
        (["--opt=test", "-a", 42], ("test", "42", ())),
        (["-o"], ("flag", None, ())),
        (["-o", "-a", 42], ("flag", "42", ())),
        (["-o", "test", "-a", 42], ("test", "42", ())),
        (["-otest", "-a", 42], ("test", "42", ())),
        (["a", "b", "c"], (None, None, ("a", "b", "c"))),
        (["--opt", "a", "b", "c"], ("a", None, ("b", "c"))),
        (["--opt", "test"], ("test", None, ())),
        (["-otest", "a", "b", "c"], ("test", None, ("a", "b", "c"))),
        (["--opt=test", "a", "b", "c"], ("test", None, ("a", "b", "c"))),
    ],
)
def test_option_with_optional_value(runner, args, expect):
    @click.command()
    @click.option("-o", "--opt", is_flag=False, flag_value="flag")
    @click.option("-a")
    @click.argument("b", nargs=-1)
    def cli(opt, a, b):
        return opt, a, b

    result = runner.invoke(cli, args, standalone_mode=False, catch_exceptions=False)
    assert result.return_value == expect


def test_multiple_option_with_optional_value(runner):
    cli = click.Command(
        "cli",
        params=[
            click.Option(["-f"], is_flag=False, flag_value="flag", multiple=True),
            click.Option(["-a"]),
            click.Argument(["b"], nargs=-1),
        ],
        callback=lambda **kwargs: kwargs,
    )
    result = runner.invoke(
        cli,
        ["-f", "-f", "other", "-f", "-a", "1", "a", "b"],
        standalone_mode=False,
        catch_exceptions=False,
    )
    assert result.return_value == {
        "f": ("flag", "other", "flag"),
        "a": "1",
        "b": ("a", "b"),
    }


def test_type_from_flag_value():
    param = click.Option(["-a", "x"], default=True, flag_value=4)
    assert param.type is click.INT
    param = click.Option(["-b", "x"], flag_value=8)
    assert param.type is click.INT
    # Non-basic types auto-detect as UNPROCESSED to avoid stringification.
    param = click.Option(["-c", "x"], flag_value=EngineType.OSS)
    assert param.type is click.UNPROCESSED
    param = click.Option(["-d", "x"], flag_value=frozenset())
    assert param.type is click.UNPROCESSED


@pytest.mark.parametrize(
    ("opt_params", "args", "expected"),
    [
        # The type passed to the option is responsible to converting the value, whether
        # we pass the option flag or not.
        ({"type": bool}, [], False),
        ({"type": bool}, ["--foo"], True),
        ({"type": click.BOOL}, [], False),
        ({"type": click.BOOL}, ["--foo"], True),
        ({"type": str}, [], None),
        ({"type": str}, ["--foo"], "True"),
        # Default value is given as-is to the --foo option when it is not passed,
        # whatever the type. Now if --foo is passed, the value is always True, whatever
        # the type. In both case the type of the option is responsible for the
        # conversion of the value.
        ({"type": bool, "default": True}, [], True),
        ({"type": bool, "default": True}, ["--foo"], True),
        ({"type": bool, "default": False}, [], False),
        ({"type": bool, "default": False}, ["--foo"], True),
        # ({"type": bool, "default": "foo"}, [], "foo"),
        ({"type": bool, "default": "foo"}, ["--foo"], True),
        ({"type": bool, "default": None}, [], None),
        ({"type": bool, "default": None}, ["--foo"], True),
        ({"type": click.BOOL, "default": True}, [], True),
        ({"type": click.BOOL, "default": True}, ["--foo"], True),
        ({"type": click.BOOL, "default": False}, [], False),
        ({"type": click.BOOL, "default": False}, ["--foo"], True),
        # ({"type": click.BOOL, "default": "foo"}, [], "foo"),
        ({"type": click.BOOL, "default": "foo"}, ["--foo"], True),
        ({"type": click.BOOL, "default": None}, [], None),
        ({"type": click.BOOL, "default": None}, ["--foo"], True),
        ({"type": str, "default": True}, [], "True"),
        ({"type": str, "default": True}, ["--foo"], "True"),
        ({"type": str, "default": False}, [], "False"),
        ({"type": str, "default": False}, ["--foo"], "True"),
        ({"type": str, "default": "foo"}, [], "foo"),
        ({"type": str, "default": "foo"}, ["--foo"], "True"),
        ({"type": str, "default": None}, [], None),
        ({"type": str, "default": None}, ["--foo"], "True"),
        # Flag value is given as-is to the --foo option when it is passed, then
        # converted by the option type.
        ({"type": bool, "flag_value": True}, [], False),
        ({"type": bool, "flag_value": True}, ["--foo"], True),
        ({"type": bool, "flag_value": False}, [], False),
        ({"type": bool, "flag_value": False}, ["--foo"], False),
        ({"type": bool, "flag_value": None}, [], False),
        ({"type": bool, "flag_value": None}, ["--foo"], None),
        ({"type": click.BOOL, "flag_value": True}, [], False),
        ({"type": click.BOOL, "flag_value": True}, ["--foo"], True),
        ({"type": click.BOOL, "flag_value": False}, [], False),
        ({"type": click.BOOL, "flag_value": False}, ["--foo"], False),
        ({"type": click.BOOL, "flag_value": None}, [], False),
        ({"type": click.BOOL, "flag_value": None}, ["--foo"], None),
        ({"type": str, "flag_value": True}, [], None),
        ({"type": str, "flag_value": True}, ["--foo"], "True"),
        ({"type": str, "flag_value": False}, [], None),
        ({"type": str, "flag_value": False}, ["--foo"], "False"),
        ({"type": str, "flag_value": "foo"}, [], None),
        ({"type": str, "flag_value": "foo"}, ["--foo"], "foo"),
        ({"type": str, "flag_value": None}, [], None),
        ({"type": str, "flag_value": None}, ["--foo"], None),
        # Not passing --foo returns the default value as-is, in its Python type, then
        # converted by the option type. For boolean flags, default=True is a literal
        # value, not a sentinel meaning "activate flag". So it is NOT substituted with
        # flag_value. See: https://github.com/pallets/click/issues/3111
        # https://github.com/pallets/click/pull/3239
        ({"type": bool, "default": True, "flag_value": True}, [], True),
        ({"type": bool, "default": True, "flag_value": False}, [], True),
        ({"type": bool, "default": False, "flag_value": True}, [], False),
        ({"type": bool, "default": False, "flag_value": False}, [], False),
        ({"type": bool, "default": None, "flag_value": True}, [], None),
        ({"type": bool, "default": None, "flag_value": False}, [], None),
        ({"type": str, "default": True, "flag_value": True}, [], "True"),
        ({"type": str, "default": True, "flag_value": False}, [], "False"),
        ({"type": str, "default": False, "flag_value": True}, [], "False"),
        ({"type": str, "default": False, "flag_value": False}, [], "False"),
        ({"type": str, "default": "foo", "flag_value": True}, [], "foo"),
        ({"type": str, "default": "foo", "flag_value": False}, [], "foo"),
        ({"type": str, "default": "foo", "flag_value": "bar"}, [], "foo"),
        ({"type": str, "default": "foo", "flag_value": None}, [], "foo"),
        ({"type": str, "default": None, "flag_value": True}, [], None),
        ({"type": str, "default": None, "flag_value": False}, [], None),
        # Passing --foo returns the flag_value that was explicitly set by the user,
        # with its Python type, but still converted by the option type.
        ({"type": bool, "default": True, "flag_value": True}, ["--foo"], True),
        ({"type": bool, "default": True, "flag_value": False}, ["--foo"], False),
        ({"type": bool, "default": False, "flag_value": True}, ["--foo"], True),
        ({"type": bool, "default": False, "flag_value": False}, ["--foo"], False),
        ({"type": bool, "default": None, "flag_value": True}, ["--foo"], True),
        ({"type": bool, "default": None, "flag_value": False}, ["--foo"], False),
        ({"type": str, "default": True, "flag_value": True}, ["--foo"], "True"),
        ({"type": str, "default": True, "flag_value": False}, ["--foo"], "False"),
        ({"type": str, "default": False, "flag_value": True}, ["--foo"], "True"),
        ({"type": str, "default": False, "flag_value": False}, ["--foo"], "False"),
        ({"type": str, "default": "foo", "flag_value": True}, ["--foo"], "True"),
        ({"type": str, "default": "foo", "flag_value": False}, ["--foo"], "False"),
        ({"type": str, "default": "foo", "flag_value": "bar"}, ["--foo"], "bar"),
        ({"type": str, "default": "foo", "flag_value": None}, ["--foo"], None),
        ({"type": str, "default": None, "flag_value": True}, ["--foo"], "True"),
        ({"type": str, "default": None, "flag_value": False}, ["--foo"], "False"),
    ],
)
def test_flag_value_and_default(runner, opt_params, args, expected):
    @click.command()
    @click.option("--foo", is_flag=True, **opt_params)
    def cmd(foo):
        click.echo(repr(foo), nl=False)

    result = runner.invoke(cmd, args)
    assert result.output == repr(expected)


@pytest.mark.parametrize(
    ("args", "opts"),
    [
        ([], {"type": bool, "default": "foo"}),
        ([], {"type": click.BOOL, "default": "foo"}),
        (["--foo"], {"type": bool, "flag_value": "foo"}),
        (["--foo"], {"type": click.BOOL, "flag_value": "foo"}),
    ],
)
def test_invalid_flag_definition(runner, args, opts):
    @click.command()
    @click.option("--foo", is_flag=True, **opts)
    def cmd(foo):
        click.echo(foo)

    result = runner.invoke(cmd, args)
    assert (
        "Error: Invalid value for '--foo': 'foo' is not a valid boolean"
        in result.output
    )


@pytest.mark.parametrize(
    ("default", "args", "expected"),
    # These test cases are similar to the ones in
    # tests/test_basic.py::test_flag_value_dual_options, so keep them in sync.
    (
        # Each option is returning its own flag_value, whatever the default is.
        (True, ["--js"], "js"),
        (True, ["--xml"], "xml"),
        (False, ["--js"], "js"),
        (False, ["--xml"], "xml"),
        (None, ["--js"], "js"),
        (None, ["--xml"], "xml"),
        (UNSET, ["--js"], "js"),
        (UNSET, ["--xml"], "xml"),
        # Check that the last option wins when both are specified.
        (True, ["--js", "--xml"], "xml"),
        (True, ["--xml", "--js"], "js"),
        # Check that the default is returned as-is when no option is specified.
        ("js", [], "js"),
        ("xml", [], "xml"),
        ("jS", [], "jS"),
        ("xMl", [], "xMl"),
        (" ᕕ( ᐛ )ᕗ ", [], " ᕕ( ᐛ )ᕗ "),
        (None, [], None),
        # Special case: UNSET is not provided as-is to the callback, but normalized to
        # None.
        (UNSET, [], None),
        # Special case: if default=True and flag_value is set, the value returned is the
        # flag_value, not the True Python value itself.
        (True, [], "js"),
        # Non-string defaults are process as strings by the default Parameter's type.
        (False, [], "False"),
        (42, [], "42"),
        (12.3, [], "12.3"),
    ),
)
def test_default_dual_option_callback(runner, default, args, expected):
    """Check how default is processed by the callback when options compete for the same
    variable name.

    Reproduction of the issue reported in
    https://github.com/pallets/click/pull/3030#discussion_r2271571819

    .. hint::
        Similar to ``test_basic.py::test_flag_value_dual_options``.

        ``test_defaults.py::test_shared_param_prefers_first_default``
        is a smoke-test complement that exercises both default placements.
    """

    def _my_func(ctx, param, value):
        # Print the value received by the callback as-is, so we can check for it.
        return f"Callback value: {value!r}"

    @click.command()
    @click.option("--js", "fmt", flag_value="js", callback=_my_func, default=default)
    @click.option("--xml", "fmt", flag_value="xml", callback=_my_func)
    def main(fmt):
        click.secho(fmt, nl=False)

    result = runner.invoke(main, args)
    assert result.output == f"Callback value: {expected!r}"
    assert result.exit_code == 0


@pytest.mark.parametrize(
    ("flag_value", "envvar_value", "expected"),
    [
        # The envvar match exactly the flag value and is case-sensitive.
        ("bar", "bar", "bar"),
        ("BAR", "BAR", "BAR"),
        (" bar ", " bar ", " bar "),
        ("42", "42", "42"),
        ("None", "None", "None"),
        ("True", "True", "True"),
        ("False", "False", "False"),
        ("true", "true", "true"),
        ("false", "false", "false"),
        ("A B", "A B", "A B"),
        (" 1 2 ", " 1 2 ", " 1 2 "),
        ("9.3", "9.3", "9.3"),
        ("a;n", "a;n", "a;n"),
        ("x:y", "x:y", "x:y"),
        ("i/o", "i/o", "i/o"),
        # Empty or absent envvar is consider unset, so the flag default value is
        # returned, which is None in this case.
        ("bar", "", None),
        ("bar", None, None),
        ("BAR", "", None),
        ("BAR", None, None),
        (" bar ", "", None),
        (" bar ", None, None),
        (42, "", None),
        (42, None, None),
        ("42", "", None),
        ("42", None, None),
        (None, "", None),
        (None, None, None),
        ("None", "", None),
        ("None", None, None),
        (True, "", None),
        (True, None, None),
        ("True", "", None),
        ("True", None, None),
        ("true", "", None),
        ("true", None, None),
        (False, "", None),
        (False, None, None),
        ("False", "", None),
        ("False", None, None),
        ("false", "", None),
        ("false", None, None),
        # Activate the flag with a value recognized as True by the envvar, which
        # returns the flag_value.
        ("bar", "True", "bar"),
        ("bar", "true", "bar"),
        ("bar", "trUe", "bar"),
        ("bar", "  TRUE  ", "bar"),
        ("bar", "1", "bar"),
        ("bar", "yes", "bar"),
        ("bar", "on", "bar"),
        ("bar", "t", "bar"),
        ("bar", "y", "bar"),
        # Deactivating the flag with a value recognized as False by the envvar, which
        # explicitly return the 'False' as the option is explicitly declared as a
        # string.
        ("bar", "False", "False"),
        ("bar", "false", "False"),
        ("bar", "faLse", "False"),
        ("bar", "  FALSE  ", "False"),
        ("bar", "0", "False"),
        ("bar", "no", "False"),
        ("bar", "off", "False"),
        ("bar", "f", "False"),
        ("bar", "n", "False"),
        # Any other value than the flag_value, or a recogned True or False value, fails
        # to explicitly activate or deactivate the flag. So the flag default value is
        # returned, which is None in this case.
        ("bar", " bar ", None),
        ("bar", "BAR", None),
        ("bar", "random", None),
        ("bar", "bar random", None),
        ("bar", "random bar", None),
        ("BAR", "bar", None),
        (" bar ", "bar", None),
        (" bar ", "BAR", None),
        (42, "42", None),
        (42, "foo", None),
        (None, "foo", None),
        ("None", "foo", None),
    ],
)
def test_envvar_string_flag_value(runner, flag_value, envvar_value, expected):
    """Ensure that flag_value is recognized by the envvar."""

    @click.command()
    @click.option("--upper", type=str, flag_value=flag_value, envvar="UPPER")
    def cmd(upper):
        click.echo(repr(upper), nl=False)

    result = runner.invoke(cmd, env={"UPPER": envvar_value})
    assert result.exit_code == 0
    assert result.output == repr(expected)


@pytest.mark.parametrize(
    ("opt_decls", "opt_params", "expect_is_flag", "expect_is_bool_flag"),
    [
        # Not boolean flags.
        ("-a", {"type": int}, False, False),
        ("-a", {"type": bool}, False, False),
        ("-a", {"default": True}, False, False),
        ("-a", {"default": False}, False, False),
        ("-a", {"flag_value": 1}, True, False),
        # Boolean flags.
        ("-a", {"is_flag": True}, True, True),
        ("-a/-A", {}, True, True),
        ("-a", {"flag_value": True}, True, True),
        # Non-flag with flag_value.
        ("-a", {"is_flag": False, "flag_value": 1}, False, False),
    ],
)
def test_flag_auto_detection(
    opt_decls, opt_params, expect_is_flag, expect_is_bool_flag
):
    option = Option([opt_decls], **opt_params)
    assert option.is_flag is expect_is_flag
    assert option.is_bool_flag is expect_is_bool_flag


@pytest.mark.parametrize(
    ("kwargs", "message"),
    [
        ({"count": True, "multiple": True}, "'count' is not valid with 'multiple'."),
        ({"count": True, "is_flag": True}, "'count' is not valid with 'is_flag'."),
    ],
)
def test_invalid_flag_combinations(runner, kwargs, message):
    with pytest.raises(TypeError) as e:
        click.Option(["-a"], **kwargs)

    assert message in str(e.value)


def test_non_flag_with_non_negatable_default(runner):
    class NonNegatable:
        def __bool__(self):
            raise ValueError("Cannot negate this object")

    @click.command()
    @click.option("--foo", default=NonNegatable())
    def cmd(foo):
        pass

    result = runner.invoke(cmd)
    assert result.exit_code == 0


class HashType(enum.Enum):
    MD5 = "MD5"
    SHA1 = "SHA1"
    SHA256 = "SHA-256"


class Number(enum.IntEnum):
    ONE = enum.auto()
    TWO = enum.auto()


class Letter(enum.StrEnum):
    NAME_1 = "Value-1"
    NAME_2 = "Value_2"
    NAME_3 = "42_value"


class Color(enum.Flag):
    RED = enum.auto()
    GREEN = enum.auto()
    BLUE = enum.auto()


class ColorInt(enum.IntFlag):
    RED = enum.auto()
    GREEN = enum.auto()
    BLUE = enum.auto()


@pytest.mark.parametrize(
    ("choices", "metavar"),
    [
        (["foo", "bar"], "[TEXT]"),
        ([1, 2], "[INTEGER]"),
        ([1.0, 2.0], "[FLOAT]"),
        ([True, False], "[BOOLEAN]"),
        (["foo", 1], "[TEXT|INTEGER]"),
        (HashType, "[HASHTYPE]"),
        (Number, "[NUMBER]"),
        (Letter, "[LETTER]"),
        (Color, "[COLOR]"),
        (ColorInt, "[COLORINT]"),
    ],
)
def test_choice_usage_rendering(runner, choices, metavar):
    """BY default ``--help`` prints choice's values in the usage message.

    But ``show_choices=False`` makes ``--help`` prints choice's METAVAR instead of
    values.

    Also check that usage error message always suggests the actual values.
    """

    @click.command()
    @click.option("-g", type=click.Choice(choices))
    def cli_with_choices(g):
        pass

    @click.command()
    @click.option("-g", type=click.Choice(choices), show_choices=False)
    def cli_without_choices(g):
        pass

    display_values = tuple(
        i.name if isinstance(i, enum.Enum) else str(i) for i in choices
    )

    # Check that the choices values are rendered as-is in the usage message.
    result = runner.invoke(cli_with_choices, ["--help"])
    assert f"[{'|'.join(display_values)}]" in result.stdout
    assert not result.stderr
    assert result.exit_code == 0

    # Check that the metavar is rendered instead of the choices values themselves.
    result = runner.invoke(cli_without_choices, ["--help"])
    assert metavar in result.stdout
    assert not result.stderr
    assert result.exit_code == 0

    # Check the usage error message suggests the actual accepted values.
    for cli in (cli_with_choices, cli_without_choices):
        result = runner.invoke(cli, ["-g", "random"])
        assert (
            "\n\nError: Invalid value for '-g': 'random' is not one of "
            f"{', '.join(map(repr, display_values))}.\n" in result.stderr
        )
        assert not result.stdout
        assert result.exit_code == 2


@pytest.mark.parametrize(
    ("choices", "default", "default_string"),
    [
        (["foo", "bar"], "bar", "bar"),
        # The default value is not enforced to be in the choices.
        (["foo", "bar"], "random", "random"),
        # None cannot be a default value as-is: it left the default value as unset.
        (["foo", "bar"], None, None),
        ([0, 1], 0, "0"),
        # Values are not coerced to the type of the choice, even if equivalent.
        ([0, 1], 0.0, "0.0"),
        ([1, 2], 2, "2"),
        ([1.0, 2.0], 2, "2"),
        ([1.0, 2.0], 2.0, "2.0"),
        ([True, False], True, "True"),
        ([True, False], False, "False"),
        (["foo", 1], "foo", "foo"),
        (["foo", 1], 1, "1"),
        # Enum choices are rendered as their names, not values.
        # See: https://github.com/pallets/click/issues/2911
        (HashType, HashType.SHA1, "SHA1"),
        # Enum choices allow defaults strings that are their names.
        (HashType, HashType.SHA256, "SHA256"),
        (HashType, "SHA256", "SHA256"),
        (Number, Number.TWO, "TWO"),
        (Number, "TWO", "TWO"),
        (Letter, Letter.NAME_1, "NAME_1"),
        (Letter, Letter.NAME_2, "NAME_2"),
        (Letter, Letter.NAME_3, "NAME_3"),
        (Letter, "NAME_1", "NAME_1"),
        (Letter, "NAME_2", "NAME_2"),
        (Letter, "NAME_3", "NAME_3"),
        (Color, Color.GREEN, "GREEN"),
        (Color, "GREEN", "GREEN"),
        (ColorInt, ColorInt.GREEN, "GREEN"),
        (ColorInt, "GREEN", "GREEN"),
    ],
)
def test_choice_default_rendering(runner, choices, default, default_string):
    @click.command()
    @click.option("-g", type=click.Choice(choices), default=default, show_default=True)
    def cli_with_choices(g):
        pass

    # Check that the default value is kept normalized to the type of the choice.
    assert cli_with_choices.params[0].default == default

    result = runner.invoke(cli_with_choices, ["--help"])
    extra_usage = f"[default: {default_string}]"
    if default_string is None:
        assert extra_usage not in result.output
    else:
        assert extra_usage in result.output


@pytest.mark.parametrize(
    "opts_one,opts_two",
    [
        # No duplicate shortnames
        (
            ("-a", "--aardvark"),
            ("-a", "--avocado"),
        ),
        # No duplicate long names
        (
            ("-a", "--aardvark"),
            ("-b", "--aardvark"),
        ),
    ],
)
def test_duplicate_names_warning(runner, opts_one, opts_two):
    @click.command()
    @click.option(*opts_one)
    @click.option(*opts_two)
    def cli(one, two):
        pass

    with pytest.warns(UserWarning):
        runner.invoke(cli, [])


OBJECT_SENTINEL = object()
"""An object-based sentinel value."""


class EnumSentinel(enum.Enum):
    FALSY_SENTINEL = object()

    def __bool__(self) -> Literal[False]:
        """Force the sentinel to be falsy to make sure it is not caught by Click
        internal implementation.

        Falsy sentinels have been discussed in:
        https://github.com/pallets/click/pull/3030#pullrequestreview-3106604795
        https://github.com/pallets/click/pull/3030#pullrequestreview-3108471552
        """
        return False


# Any kind of sentinel value is recognized by Click as a valid flag value.
@pytest.mark.parametrize("sentinel", (OBJECT_SENTINEL, EnumSentinel.FALSY_SENTINEL))
@pytest.mark.parametrize(
    ("args", "expected"),
    (
        # Option default to None.
        ([], None),
        # Config has no default value, and no flag value, so it requires an argument.
        (
            ["--config"],
            re.compile(re.escape("Error: Option '--config' requires an argument.\n")),
        ),
        (
            ["--no-config", "--config"],
            re.compile(re.escape("Error: Option '--config' requires an argument.\n")),
        ),
        # Passing --no-config defaults to the sentinel value because of the flag_value,
        # and then the custom type receives that sentinel and returns a message.
        (["--no-config"], "No configuration file provided."),
        # Passing --config with an argument returns the file path.
        (["--config", "foo.conf"], "foo.conf"),
        # An argument is not allowed for --no-config, so it raises an error.
        (
            ["--no-config", "foo.conf"],
            re.compile(
                r"Usage: main \[OPTIONS\]\n"
                r"Try 'main --help' for help.\n"
                r"\n"
                r"Error: Got unexpected extra argument (.+)\n"
            ),
        ),
        # Passing --config with an argument that does not exist raises an error.
        (
            ["--config", "random-file.conf"],
            re.compile(
                r"Usage: main \[OPTIONS\]\n"
                r"Try 'main --help' for help.\n"
                r"\n"
                r"Error: Invalid value for '-c' / '--config': "
                r"File 'random-file.conf' does not exist.\n"
            ),
        ),
        (
            ["--config", "--no-config"],
            re.compile(
                r"Usage: main \[OPTIONS\]\n"
                r"Try 'main --help' for help.\n"
                r"\n"
                r"Error: Invalid value for '-c' / '--config': "
                r"File '--no-config' does not exist.\n"
            ),
        ),
        (
            ["--config", "--no-config", "foo.conf"],
            re.compile(
                r"Usage: main \[OPTIONS\]\n"
                r"Try 'main --help' for help.\n"
                r"\n"
                r"Error: Invalid value for '-c' / '--config': "
                r"File '--no-config' does not exist.\n"
            ),
        ),
        # --config is passed last and overrides the --no-config option.
        (["--no-config", "--config", "foo.conf"], "foo.conf"),
    ),
)
def test_dual_options_custom_type_sentinel_flag_value(runner, sentinel, args, expected):
    """Check that an object-based sentinel, used as a flag value, is returned as-is
    to a custom type that is shared by two options, competing for the same variable
    name.

    A reproduction of
    https://github.com/pallets/click/issues/3024#issuecomment-3146511356
    """

    class ConfigParamType(click.ParamType):
        """A custom type that accepts a file path or a sentinel value."""

        def convert(self, value, param, ctx):
            if value is sentinel:
                return "No configuration file provided."
            else:
                return click.Path(exists=True, dir_okay=False).convert(
                    value, param, ctx
                )

    @click.command()
    @click.option("-c", "--config", type=ConfigParamType())
    @click.option("--no-config", "config", flag_value=sentinel, type=ConfigParamType())
    def main(config):
        click.echo(repr(config), nl=False)

    with tempfile.NamedTemporaryFile(mode="w") as named_tempfile:
        if "foo.conf" in args:
            named_tempfile.write("Blah blah")
            named_tempfile.flush()
            args = [named_tempfile.name if a == "foo.conf" else a for a in args]

        result = runner.invoke(main, args)

        if isinstance(expected, re.Pattern):
            assert re.match(expected, result.output)
        else:
            assert result.output == repr(
                named_tempfile.name if expected == "foo.conf" else expected
            )


class EngineType(enum.Enum):
    OSS = enum.auto()
    PRO = enum.auto()
    MAX = enum.auto()


class Class1:
    pass


class Class2:
    pass


@pytest.mark.parametrize(
    ("opt_params", "args", "expected"),
    [
        # Check that the flag value is returned as-is when the option is passed, and
        # not normalized to a boolean, even if it is explicitly declared as a flag.
        ({"type": EngineType, "flag_value": None}, ["--pro"], None),
        (
            {"type": EngineType, "is_flag": True, "flag_value": None},
            ["--pro"],
            None,
        ),
        ({"type": EngineType, "flag_value": EngineType.OSS}, ["--pro"], EngineType.OSS),
        (
            {"type": EngineType, "is_flag": True, "flag_value": EngineType.OSS},
            ["--pro"],
            EngineType.OSS,
        ),
        (
            {"type": EngineType, "is_flag": True, "default": EngineType.OSS},
            ["--pro"],
            EngineType.OSS,
        ),
        # The default value is returned as-is when the option is not passed, whatever
        # the flag value.
        ({"type": EngineType, "flag_value": None}, [], None),
        ({"type": EngineType, "is_flag": True, "flag_value": None}, [], None),
        ({"type": EngineType, "flag_value": EngineType.OSS}, [], None),
        (
            {"type": EngineType, "is_flag": True, "flag_value": EngineType.OSS},
            [],
            None,
        ),
        (
            {"type": EngineType, "is_flag": True, "default": EngineType.OSS},
            [],
            EngineType.OSS,
        ),
        # The option has not enough parameters to be detected as flag-like, so it
        # requires an argument.
        (
            {"type": EngineType, "default": EngineType.OSS},
            ["--pro"],
            re.compile(re.escape("Error: Option '--pro' requires an argument.\n")),
        ),
        ({"type": EngineType, "default": EngineType.OSS}, [], EngineType.OSS),
        # If a flag value is set, it is returned instead of the default value.
        (
            {"type": EngineType, "flag_value": EngineType.OSS, "default": True},
            ["--pro"],
            EngineType.OSS,
        ),
        (
            {"type": EngineType, "flag_value": EngineType.OSS, "default": True},
            [],
            EngineType.OSS,
        ),
        # Type is not specified. For string flag_value, STRING type is used and
        # the default value is converted to string. For non-basic types (like
        # enums), UNPROCESSED is used and values pass through unchanged.
        ({"flag_value": "1", "default": True}, [], "1"),
        ({"flag_value": "1", "default": 42}, [], "42"),
        ({"flag_value": EngineType.OSS, "default": True}, [], EngineType.OSS),
        ({"flag_value": EngineType.OSS, "default": 42}, [], 42),
        # See: the result is the same if we force the type to be str.
        ({"type": str, "flag_value": 1, "default": True}, [], "1"),
        ({"type": str, "flag_value": 1, "default": 42}, [], "42"),
        ({"type": str, "flag_value": "1", "default": True}, [], "1"),
        ({"type": str, "flag_value": "1", "default": 42}, [], "42"),
        (
            {"type": str, "flag_value": EngineType.OSS, "default": True},
            [],
            "EngineType.OSS",
        ),
        ({"type": str, "flag_value": EngineType.OSS, "default": 42}, [], "42"),
        # But having the flag value set to integer is automaticcally recognized by
        # Click.
        ({"flag_value": 1, "default": True}, [], 1),
        ({"flag_value": 1, "default": 42}, [], 42),
        ({"type": int, "flag_value": 1, "default": True}, [], 1),
        ({"type": int, "flag_value": 1, "default": 42}, [], 42),
        ({"type": int, "flag_value": "1", "default": True}, [], 1),
        ({"type": int, "flag_value": "1", "default": 42}, [], 42),
    ],
)
def test_custom_type_flag_value_standalone_option(runner, opt_params, args, expected):
    """Test how the type and flag_value influence the returned value.

    Cover cases reported in:
    https://github.com/pallets/click/issues/3024#issuecomment-3146480714
    https://github.com/pallets/click/issues/2012#issuecomment-892437060
    """

    @click.command()
    @click.option("--pro", **opt_params)
    def scan(pro):
        click.echo(repr(pro), nl=False)

    result = runner.invoke(scan, args)
    if isinstance(expected, re.Pattern):
        assert re.match(expected, result.output)
    else:
        assert result.output == repr(expected)


@pytest.mark.parametrize(
    ("opt1_params", "opt2_params", "args", "expected"),
    [
        # Dual options sharing the same variable name, are not competitive, and the
        # flag value is returned as-is. Especially when the type is force to be
        # unprocessed.
        (
            {"flag_value": EngineType.OSS, "type": UNPROCESSED},
            {"flag_value": EngineType.PRO, "type": UNPROCESSED},
            [],
            None,
        ),
        (
            {"flag_value": EngineType.OSS, "type": UNPROCESSED},
            {"flag_value": EngineType.PRO, "type": UNPROCESSED},
            ["--opt1"],
            EngineType.OSS,
        ),
        (
            {"flag_value": EngineType.OSS, "type": UNPROCESSED},
            {"flag_value": EngineType.PRO, "type": UNPROCESSED},
            ["--opt2"],
            EngineType.PRO,
        ),
        # Exotic flag values like classes are passed through unchanged when no
        # explicit type is given (UNPROCESSED is auto-detected).
        # https://github.com/pallets/click/issues/2012
        # https://github.com/pallets/click/issues/3121
        (
            {"flag_value": Class1, "default": True},
            {"flag_value": Class2},
            [],
            Class1,
        ),
        (
            {"flag_value": Class1, "default": True},
            {"flag_value": Class2},
            ["--opt1"],
            Class1,
        ),
        (
            {"flag_value": Class1, "default": True},
            {"flag_value": Class2},
            ["--opt2"],
            Class2,
        ),
        # String and None defaults pass through unchanged.
        ({"flag_value": Class1, "default": "True"}, {"flag_value": Class2}, [], "True"),
        ({"flag_value": Class1, "default": None}, {"flag_value": Class2}, [], None),
        # To get the classes as-is, we need to specify the type as UNPROCESSED.
        # https://github.com/pallets/click/issues/3121
        (
            {"flag_value": Class1, "type": UNPROCESSED, "default": True},
            {"flag_value": Class2, "type": UNPROCESSED},
            [],
            Class1,
        ),
        (
            {"flag_value": Class1, "type": UNPROCESSED, "default": True},
            {"flag_value": Class2, "type": UNPROCESSED},
            ["--opt1"],
            Class1,
        ),
        (
            {"flag_value": Class1, "type": UNPROCESSED, "default": True},
            {"flag_value": Class2, "type": UNPROCESSED},
            ["--opt2"],
            Class2,
        ),
        # Setting the default to a class, an instance of the class is returned instead
        # of the class itself, because the default is allowed to be callable (and
        # consumed). And this happens whatever the type is.
        (
            {"flag_value": Class1, "default": Class1},
            {"flag_value": Class2},
            [],
            re.compile(r"<test_options.Class1 object at 0x[0-9A-Fa-f]+>"),
        ),
        (
            {"flag_value": Class1, "default": Class2},
            {"flag_value": Class2},
            [],
            re.compile(r"<test_options.Class2 object at 0x[0-9A-Fa-f]+>"),
        ),
        (
            {"flag_value": Class1, "type": UNPROCESSED, "default": Class1},
            {"flag_value": Class2, "type": UNPROCESSED},
            [],
            re.compile(r"<test_options.Class1 object at 0x[0-9A-Fa-f]+>"),
        ),
        (
            {"flag_value": Class1, "type": UNPROCESSED, "default": Class2},
            {"flag_value": Class2, "type": UNPROCESSED},
            [],
            re.compile(r"<test_options.Class2 object at 0x[0-9A-Fa-f]+>"),
        ),
        # Having the flag value set to integer is automaticcally recognized by Click.
        (
            {"flag_value": 1, "default": True},
            {"flag_value": "1"},
            [],
            1,
        ),
        (
            {"flag_value": 1, "type": int, "default": True},
            {"flag_value": "1", "type": int},
            [],
            1,
        ),
    ],
)
def test_custom_type_flag_value_dual_options(
    runner, opt1_params, opt2_params, args, expected
):
    """Test how flag values are processed with dual options competing for the same
    variable name.

    Reproduce issues reported in:
    https://github.com/pallets/click/issues/3024#issuecomment-3146508536
    https://github.com/pallets/click/issues/2012#issue-946471049
    https://github.com/pallets/click/issues/2012#issuecomment-892437060
    """

    @click.command()
    @click.option("--opt1", "dual_option", **opt1_params)
    @click.option("--opt2", "dual_option", **opt2_params)
    def cli(dual_option):
        click.echo(repr(dual_option), nl=False)

    result = runner.invoke(cli, args)
    if isinstance(expected, re.Pattern):
        assert re.match(expected, result.output)
    else:
        assert result.output == repr(expected)


@pytest.mark.parametrize(
    ("opt_params", "args", "expected"),
    [
        # Class flag_value with default=True and UNPROCESSED: the class itself is
        # returned, NOT an instance. Regression test for
        # https://github.com/pallets/click/issues/3121
        ({"flag_value": Class1, "type": UNPROCESSED, "default": True}, [], Class1),
        (
            {"flag_value": Class1, "type": UNPROCESSED, "default": True},
            ["--opt"],
            Class1,
        ),
        # Without explicit UNPROCESSED, the class still passes through unchanged
        # because UNPROCESSED is auto-detected for non-basic flag_value types.
        ({"flag_value": Class1, "default": True}, [], Class1),
        (
            {"flag_value": Class1, "default": True},
            ["--opt"],
            Class1,
        ),
        # Explicit default=Class1 (not via default=True alignment): callable IS invoked,
        # because the user explicitly set a callable as the default.
        (
            {"flag_value": Class1, "type": UNPROCESSED, "default": Class1},
            [],
            re.compile(r"<test_options.Class1 object at 0x[0-9A-Fa-f]+>"),
        ),
        # Explicit default=Class2, different from flag_value=Class1.
        (
            {"flag_value": Class1, "type": UNPROCESSED, "default": Class2},
            [],
            re.compile(r"<test_options.Class2 object at 0x[0-9A-Fa-f]+>"),
        ),
        # Non-callable flag_value with default=True: unaffected by the fix.
        ({"flag_value": "upper", "default": True}, [], "upper"),
        ({"flag_value": "upper", "default": True}, ["--opt"], "upper"),
    ],
)
def test_callable_flag_value_not_instantiated(runner, opt_params, args, expected):
    """A callable ``flag_value`` like a class, with ``default=True`` should not be
    invoked when resolving the default. This is the single-option variant of
    the regression reported in https://github.com/pallets/click/issues/3121.
    """

    @click.command()
    @click.option("--opt", "value", **opt_params)
    def cli(value):
        click.echo(repr(value), nl=False)

    result = runner.invoke(cli, args)
    assert result.exit_code == 0
    if isinstance(expected, re.Pattern):
        assert re.match(expected, result.output)
    else:
        assert result.output == repr(expected)


def test_callable_flag_value_default_map(runner):
    """A ``default_map`` entry should override the auto-aligned callable ``flag_value``.

    When ``default=True`` and ``flag_value=SomeClass``, the default is aligned to
    ``SomeClass``. If ``default_map`` provides a different value (including a
    callable), it should take precedence and callables from ``default_map`` should
    still be invoked.
    """

    @click.command()
    @click.option("--opt", "value", flag_value=Class1, type=UNPROCESSED, default=True)
    def cli(value):
        click.echo(repr(value), nl=False)

    # Static value in default_map overrides the flag_value default.
    result = runner.invoke(cli, [], default_map={"value": "from-map"})
    assert result.output == repr("from-map")

    # Callable in default_map is still invoked (not suppressed by the fix).
    result = runner.invoke(cli, [], default_map={"value": lambda: "lazy-map"})
    assert result.output == repr("lazy-map")

    # CLI arg still wins over everything.
    result = runner.invoke(cli, ["--opt"])
    assert result.output == repr(Class1)


def test_callable_flag_value_show_default(runner):
    """Help text with ``show_default=True`` should display the class name, not
    instantiate it.
    """

    @click.command()
    @click.option(
        "--opt",
        "value",
        flag_value=Class1,
        type=UNPROCESSED,
        default=True,
        show_default=True,
    )
    def cli(value):
        pass

    result = runner.invoke(cli, ["--help"])
    assert result.exit_code == 0
    assert "Class1" in result.output
    assert "object at 0x" not in result.output


@pytest.mark.parametrize(
    ("opt_params", "expected_default_attr", "expected_get_default"),
    [
        # default=True with callable flag_value: the attribute stays True
        # (not eagerly aligned), but get_default() resolves to the flag_value.
        (
            {"flag_value": Class1, "type": UNPROCESSED, "default": True},
            True,
            Class1,
        ),
        # default=True with non-callable flag_value: same lazy resolution.
        (
            {"flag_value": "upper", "default": True},
            True,
            "upper",
        ),
        # Explicit default (not True): attribute and get_default() agree.
        (
            {"flag_value": Class1, "type": UNPROCESSED, "default": "custom"},
            "custom",
            "custom",
        ),
        # No default: attribute and get_default() are both UNSET.
        (
            {"flag_value": Class1, "type": UNPROCESSED},
            UNSET,
            UNSET,
        ),
    ],
)
def test_callable_flag_value_get_default_override(
    runner, opt_params, expected_default_attr, expected_get_default
):
    """The ``default=True`` to ``flag_value`` alignment is resolved lazily in
    ``get_default()`` rather than eagerly in ``__init__``. This means
    ``option.default`` stays as ``True`` while ``get_default()`` returns the
    ``flag_value``.

    A user subclass that reads ``self.default`` directly (bypassing
    ``get_default()``) will see ``True`` instead of the ``flag_value``.
    """

    @click.command()
    @click.option("--opt", "value", **opt_params)
    def cli(value):
        pass

    opt = cli.params[0]

    # The raw attribute reflects what the user wrote.
    assert opt.default is expected_default_attr

    # get_default() resolves the alignment lazily.
    ctx = click.Context(cli)
    assert opt.get_default(ctx, call=True) is expected_get_default


def test_flag_value_not_stringified_for_custom_types(runner):
    """Non-basic flag_value types are passed through unchanged without
    requiring ``type=click.UNPROCESSED``.

    Regression test for https://github.com/pallets/click/issues/2012
    """

    @click.command()
    @click.option("--cls1", "config_cls", flag_value=Class1, default=True)
    @click.option("--cls2", "config_cls", flag_value=Class2)
    def cli(config_cls):
        click.echo(repr(config_cls), nl=False)

    # Default activates --cls1 (default=True resolves to flag_value).
    result = runner.invoke(cli, [])
    assert result.exit_code == 0
    assert result.output == repr(Class1)

    result = runner.invoke(cli, ["--cls1"])
    assert result.exit_code == 0
    assert result.output == repr(Class1)

    result = runner.invoke(cli, ["--cls2"])
    assert result.exit_code == 0
    assert result.output == repr(Class2)

    # Enum flag_value without explicit type is also preserved.
    @click.command()
    @click.option("--oss", "engine", flag_value=EngineType.OSS, default=True)
    @click.option("--pro", "engine", flag_value=EngineType.PRO)
    def cli2(engine):
        click.echo(repr(engine), nl=False)

    result = runner.invoke(cli2, [])
    assert result.exit_code == 0
    assert result.output == repr(EngineType.OSS)

    result = runner.invoke(cli2, ["--pro"])
    assert result.exit_code == 0
    assert result.output == repr(EngineType.PRO)


def test_custom_type_frozenset_flag_value(runner):
    """Check that frozenset is correctly handled as a type, a flag value and a default.

    Reproduces https://github.com/pallets/click/issues/2610
    """

    @click.command()
    @click.option(
        "--without-scm-ignore-files",
        "scm_ignore_files",
        is_flag=True,
        type=frozenset,
        flag_value=frozenset(),
        default=frozenset(["git"]),
    )
    def rcli(scm_ignore_files):
        click.echo(repr(scm_ignore_files), nl=False)

    result = runner.invoke(rcli)
    assert result.stdout == "frozenset({'git'})"
    assert result.exit_code == 0

    result = runner.invoke(rcli, ["--without-scm-ignore-files"])
    assert result.stdout == "frozenset()"
    assert result.exit_code == 0


@pytest.mark.parametrize(
    ("default", "args", "expected"),
    [
        # default=None: 3-state pattern (e.g. Flask --reload/--no-reload).
        # https://github.com/pallets/click/issues/3024
        (None, [], None),
        (None, ["--flag"], True),
        (None, ["--no-flag"], False),
        # default=True: literal value, not substituted with flag_value.
        # https://github.com/pallets/click/issues/3111
        (True, [], True),
        (True, ["--flag"], True),
        (True, ["--no-flag"], False),
    ],
)
def test_bool_flag_pair_default(runner, default, args, expected):
    """Boolean flag pairs pass ``default`` through literally.

    Ensures ``default=True`` is not replaced by ``flag_value`` for boolean
    flags, and that ``default=None`` enables 3-state logic.
    """

    @click.command()
    @click.option("--flag/--no-flag", default=default)
    def cli(flag):
        click.echo(repr(flag), nl=False)

    result = runner.invoke(cli, args)
    assert result.exit_code == 0
    assert result.output == repr(expected)


@pytest.mark.parametrize(
    ("opts", "args", "expected"),
    [
        # #3403 reproducer: enable/disable pair with explicit ``default=True``
        # on the positive flag, declared after (inner decorator) the negative.
        # https://github.com/pallets/click/issues/3403
        pytest.param(
            [
                ("--without-xyz", {"flag_value": False}),
                ("--with-xyz", {"flag_value": True, "default": True}),
            ],
            [],
            True,
            id="3403-reproducer-no-args",
        ),
        pytest.param(
            [
                ("--without-xyz", {"flag_value": False}),
                ("--with-xyz", {"flag_value": True, "default": True}),
            ],
            ["--with-xyz"],
            True,
            id="3403-reproducer-with-only",
        ),
        pytest.param(
            [
                ("--without-xyz", {"flag_value": False}),
                ("--with-xyz", {"flag_value": True, "default": True}),
            ],
            ["--without-xyz"],
            False,
            id="3403-reproducer-without-only",
        ),
        # When both flags are passed, the parser keeps the last value seen.
        pytest.param(
            [
                ("--without-xyz", {"flag_value": False}),
                ("--with-xyz", {"flag_value": True, "default": True}),
            ],
            ["--with-xyz", "--without-xyz"],
            False,
            id="3403-reproducer-with-then-without",
        ),
        pytest.param(
            [
                ("--without-xyz", {"flag_value": False}),
                ("--with-xyz", {"flag_value": True, "default": True}),
            ],
            ["--without-xyz", "--with-xyz"],
            True,
            id="3403-reproducer-without-then-with",
        ),
        # Order-independence: explicit ``default=True`` on the OUTER
        # decorator (declared first) must produce the same behavior.
        pytest.param(
            [
                ("--with-xyz", {"flag_value": True, "default": True}),
                ("--without-xyz", {"flag_value": False}),
            ],
            [],
            True,
            id="explicit-default-outer-no-args",
        ),
        pytest.param(
            [
                ("--with-xyz", {"flag_value": True, "default": True}),
                ("--without-xyz", {"flag_value": False}),
            ],
            ["--without-xyz"],
            False,
            id="explicit-default-outer-cmdline-overrides",
        ),
        # Explicit ``default=False`` on the negative flag wins over the
        # auto-derived default of the positive one. Result value is False
        # either way, but the assertion still guards source tracking.
        pytest.param(
            [
                ("--with-xyz", {"flag_value": True}),
                ("--without-xyz", {"flag_value": False, "default": False}),
            ],
            [],
            False,
            id="explicit-default-false-on-negative",
        ),
        # Explicit ``default=True`` on the negative flag is unusual but
        # legal: post-#3239, it is a literal Python value, not a sentinel.
        pytest.param(
            [
                ("--with-xyz", {"flag_value": True}),
                ("--without-xyz", {"flag_value": False, "default": True}),
            ],
            [],
            True,
            id="explicit-default-true-on-negative",
        ),
        # Both options carry an explicit default: last-wins, so the option
        # declared last in the source code keeps the slot. Confirms the
        # explicit-beats-auto tie-break does not also promote first-declared
        # over a later explicit default.
        pytest.param(
            [
                ("--with-xyz", {"flag_value": True, "default": True}),
                ("--without-xyz", {"flag_value": False, "default": False}),
            ],
            [],
            False,
            id="both-explicit-defaults-last-wins",
        ),
        pytest.param(
            [
                ("--without-xyz", {"flag_value": False, "default": False}),
                ("--with-xyz", {"flag_value": True, "default": True}),
            ],
            [],
            True,
            id="both-explicit-defaults-last-wins-swapped",
        ),
        # No option has an explicit default: every boolean flag
        # auto-derives ``default=False`` regardless of its ``flag_value``,
        # so the slot is False either way. Last-wins applies under the hood
        # but is not observable because both values are equal.
        pytest.param(
            [
                ("--with-xyz", {"flag_value": True}),
                ("--without-xyz", {"flag_value": False}),
            ],
            [],
            False,
            id="both-auto-defaults-positive-first",
        ),
        pytest.param(
            [
                ("--without-xyz", {"flag_value": False}),
                ("--with-xyz", {"flag_value": True}),
            ],
            [],
            False,
            id="both-auto-defaults-negative-first",
        ),
        # Explicit ``default=False`` matching the auto-derived value:
        # the explicit option still wins the slot. Confirms tracking is
        # source-based and not value-based.
        pytest.param(
            [
                ("--with-xyz", {"flag_value": True, "default": False}),
                ("--without-xyz", {"flag_value": False}),
            ],
            [],
            False,
            id="explicit-default-matches-auto-still-wins",
        ),
        # Three-flag group: the explicit default wins regardless of its
        # position in the decorator stack.
        pytest.param(
            [
                ("--auto-a", {"flag_value": True}),
                ("--explicit", {"flag_value": False, "default": False}),
                ("--auto-b", {"flag_value": True}),
            ],
            [],
            False,
            id="three-flags-explicit-in-middle",
        ),
        pytest.param(
            [
                ("--auto-a", {"flag_value": True}),
                ("--auto-b", {"flag_value": False}),
                ("--explicit", {"flag_value": True, "default": True}),
            ],
            [],
            True,
            id="three-flags-explicit-last",
        ),
        pytest.param(
            [
                ("--explicit", {"flag_value": False, "default": False}),
                ("--auto-a", {"flag_value": True}),
                ("--auto-b", {"flag_value": True}),
            ],
            [],
            False,
            id="three-flags-explicit-first",
        ),
        # Three-state pattern: explicit ``default=None`` on one option
        # must beat a sibling's auto-derived ``False``.
        pytest.param(
            [
                ("--without-xyz", {"flag_value": False}),
                ("--with-xyz", {"flag_value": True, "default": None}),
            ],
            [],
            None,
            id="explicit-default-none-three-state",
        ),
        pytest.param(
            [
                ("--without-xyz", {"flag_value": False}),
                ("--with-xyz", {"flag_value": True, "default": None}),
            ],
            ["--with-xyz"],
            True,
            id="explicit-default-none-three-state-with",
        ),
        pytest.param(
            [
                ("--without-xyz", {"flag_value": False}),
                ("--with-xyz", {"flag_value": True, "default": None}),
            ],
            ["--without-xyz"],
            False,
            id="explicit-default-none-three-state-without",
        ),
        # Command-line input always beats any default, regardless of
        # which option carried the explicit default.
        pytest.param(
            [
                ("--with-xyz", {"flag_value": True, "default": True}),
                ("--without-xyz", {"flag_value": False}),
            ],
            ["--without-xyz"],
            False,
            id="cmdline-beats-explicit-default",
        ),
        pytest.param(
            [
                ("--without-xyz", {"flag_value": False, "default": False}),
                ("--with-xyz", {"flag_value": True}),
            ],
            ["--with-xyz"],
            True,
            id="cmdline-beats-explicit-default-symmetric",
        ),
    ],
)
def test_bool_flag_group_competition(runner, opts, args, expected):
    """Competing boolean flags sharing a single parameter name.

    Verifies the arbitration rules between options that target the same
    variable name in a feature-switch group.

    Regression test for https://github.com/pallets/click/issues/3403
    """

    @click.command()
    def cli(enable_xyz):
        click.echo(repr(enable_xyz), nl=False)

    for opt_name, opt_kwargs in opts:
        cli = click.option(opt_name, "enable_xyz", **opt_kwargs)(cli)

    result = runner.invoke(cli, args)
    assert result.exit_code == 0, result.output
    assert result.output == repr(expected)


@pytest.mark.parametrize(
    ("envvar_value", "args", "expected"),
    [
        # An env var on one option in the group provides ENVIRONMENT source,
        # which beats any sibling's DEFAULT regardless of explicit-default.
        ("1", [], True),
        ("0", [], False),
        # Command-line still beats the env var.
        ("0", ["--with-xyz"], True),
        ("1", ["--without-xyz"], False),
    ],
)
def test_bool_flag_group_competition_with_envvar(
    runner, monkeypatch, envvar_value, args, expected
):
    monkeypatch.setenv("XYZ", envvar_value)

    @click.command()
    @click.option("--without-xyz", "enable_xyz", flag_value=False)
    @click.option(
        "--with-xyz",
        "enable_xyz",
        flag_value=True,
        default=False,
        envvar="XYZ",
    )
    def cli(enable_xyz):
        click.echo(repr(enable_xyz), nl=False)

    result = runner.invoke(cli, args)
    assert result.exit_code == 0, result.output
    assert result.output == repr(expected)


@pytest.mark.parametrize(
    ("default_map", "args", "expected"),
    [
        # ``default_map`` provides DEFAULT_MAP source, beating either default.
        ({"enable_xyz": True}, [], True),
        ({"enable_xyz": False}, [], False),
        # Command-line still beats default_map.
        ({"enable_xyz": False}, ["--with-xyz"], True),
        ({"enable_xyz": True}, ["--without-xyz"], False),
    ],
)
def test_bool_flag_group_competition_with_default_map(
    runner, default_map, args, expected
):
    @click.command()
    @click.option("--without-xyz", "enable_xyz", flag_value=False)
    @click.option("--with-xyz", "enable_xyz", flag_value=True, default=True)
    def cli(enable_xyz):
        click.echo(repr(enable_xyz), nl=False)

    result = runner.invoke(cli, args, default_map=default_map)
    assert result.exit_code == 0, result.output
    assert result.output == repr(expected)


@pytest.mark.parametrize(
    ("opts", "args", "invoke_kwargs", "expected_value", "expected_source"),
    [
        # https://github.com/pallets/click/issues/3458
        pytest.param(
            [
                ("--without-xyz", {"flag_value": False}),
                ("--with-xyz", {"flag_value": True, "default": True}),
            ],
            [],
            {},
            True,
            "DEFAULT",
            id="explicit-default-wins",
        ),
        pytest.param(
            [
                ("--without-xyz", {"flag_value": False}),
                ("--with-xyz", {"flag_value": True, "default": True}),
            ],
            ["--without-xyz"],
            {},
            False,
            "COMMANDLINE",
            id="cmdline-wins",
        ),
        pytest.param(
            [
                ("--without-xyz", {"flag_value": False}),
                ("--with-xyz", {"flag_value": True, "default": True}),
            ],
            ["--without-xyz"],
            {"default_map": {"enable_xyz": True}},
            False,
            "COMMANDLINE",
            id="loser-default-map-restores-winner-source",
        ),
    ],
)
def test_bool_flag_group_parameter_source(
    runner, opts, args, invoke_kwargs, expected_value, expected_source
):
    """``get_parameter_source()`` stays correct for feature-switch groups.

    Regression test for https://github.com/pallets/click/issues/3458.
    """

    @click.command()
    @click.pass_context
    def cli(ctx, enable_xyz):
        source = ctx.get_parameter_source("enable_xyz")
        click.echo(f"value={enable_xyz!r} source={source.name}")

    for opt_name, opt_kwargs in opts:
        cli = click.option(opt_name, "enable_xyz", **opt_kwargs)(cli)

    result = runner.invoke(cli, args, **invoke_kwargs)
    assert result.exit_code == 0, result.output
    assert f"value={expected_value!r}" in result.output
    assert f"source={expected_source}" in result.output


@pytest.mark.parametrize(
    ("opts", "args", "expected"),
    [
        # Non-boolean feature switch group: classic --upper/--lower
        # pattern. The option with ``default=True`` acts as the default
        # via the substitution rule (#3239) for non-boolean ``flag_value``.
        pytest.param(
            [
                ("--upper", {"flag_value": "upper", "default": True}),
                ("--lower", {"flag_value": "lower"}),
            ],
            [],
            "upper",
            id="string-default-true-substitutes-to-flag-value",
        ),
        pytest.param(
            [
                ("--upper", {"flag_value": "upper", "default": True}),
                ("--lower", {"flag_value": "lower"}),
            ],
            ["--upper"],
            "upper",
            id="string-default-true-cmdline-positive",
        ),
        pytest.param(
            [
                ("--upper", {"flag_value": "upper", "default": True}),
                ("--lower", {"flag_value": "lower"}),
            ],
            ["--lower"],
            "lower",
            id="string-default-true-cmdline-overrides",
        ),
        # Explicit literal string default beats sibling's absent default.
        # Confirms the explicit-beats-absent rule applies regardless of type.
        pytest.param(
            [
                ("--upper", {"flag_value": "upper", "default": "lower"}),
                ("--lower", {"flag_value": "lower"}),
            ],
            [],
            "lower",
            id="string-explicit-default-wins-over-absent",
        ),
        pytest.param(
            [
                ("--upper", {"flag_value": "upper"}),
                ("--lower", {"flag_value": "lower", "default": "upper"}),
            ],
            [],
            "upper",
            id="string-explicit-default-wins-from-second-position",
        ),
        # Empty string as ``flag_value``: still a legal value, including
        # under the ``default=True`` substitution rule.
        pytest.param(
            [
                ("--empty", {"flag_value": "", "default": True}),
                ("--filled", {"flag_value": "filled"}),
            ],
            [],
            "",
            id="empty-string-flag-value-default-true",
        ),
        pytest.param(
            [
                ("--empty", {"flag_value": "", "default": True}),
                ("--filled", {"flag_value": "filled"}),
            ],
            ["--empty"],
            "",
            id="empty-string-flag-value-cmdline",
        ),
        # Empty string as ``default``: explicit empty string beats
        # the sibling's absent default.
        pytest.param(
            [
                ("--upper", {"flag_value": "upper", "default": ""}),
                ("--lower", {"flag_value": "lower"}),
            ],
            [],
            "",
            id="empty-string-explicit-default",
        ),
        # ``flag_value=None`` is a legal flag value: when the option is
        # activated, the function receives ``None``.
        pytest.param(
            [
                ("--none", {"flag_value": None, "default": "fallback"}),
                ("--other", {"flag_value": "other"}),
            ],
            [],
            "fallback",
            id="none-flag-value-default-fallback",
        ),
        pytest.param(
            [
                ("--none", {"flag_value": None, "default": "fallback"}),
                ("--other", {"flag_value": "other"}),
            ],
            ["--none"],
            None,
            id="none-flag-value-cmdline-passes-none",
        ),
        pytest.param(
            [
                ("--none", {"flag_value": None, "default": "fallback"}),
                ("--other", {"flag_value": "other"}),
            ],
            ["--other"],
            "other",
            id="none-flag-value-cmdline-passes-other",
        ),
        # Explicit ``default=None`` is a real value (not absence) and
        # must beat a sibling's absent default. Three-state pattern for
        # non-boolean flag groups.
        pytest.param(
            [
                ("--upper", {"flag_value": "upper", "default": None}),
                ("--lower", {"flag_value": "lower"}),
            ],
            [],
            None,
            id="explicit-default-none-three-state",
        ),
        pytest.param(
            [
                ("--upper", {"flag_value": "upper", "default": None}),
                ("--lower", {"flag_value": "lower"}),
            ],
            ["--upper"],
            "upper",
            id="explicit-default-none-cmdline-upper",
        ),
        pytest.param(
            [
                ("--upper", {"flag_value": "upper", "default": None}),
                ("--lower", {"flag_value": "lower"}),
            ],
            ["--lower"],
            "lower",
            id="explicit-default-none-cmdline-lower",
        ),
        # Passing ``default=UNSET`` explicitly is the same as not passing
        # ``default`` at all, so the sibling's explicit default wins.
        pytest.param(
            [
                ("--upper", {"flag_value": "upper", "default": UNSET}),
                ("--lower", {"flag_value": "lower", "default": "lower"}),
            ],
            [],
            "lower",
            id="unset-default-equivalent-to-absent",
        ),
        pytest.param(
            [
                ("--upper", {"flag_value": "upper", "default": UNSET}),
                ("--lower", {"flag_value": "lower", "default": "lower"}),
            ],
            ["--upper"],
            "upper",
            id="unset-default-cmdline-still-works",
        ),
        # Neither option has a default: the slot resolves to ``None``
        # because non-boolean flags do not auto-derive a default.
        pytest.param(
            [
                ("--upper", {"flag_value": "upper"}),
                ("--lower", {"flag_value": "lower"}),
            ],
            [],
            None,
            id="non-boolean-no-defaults-resolves-to-none",
        ),
        pytest.param(
            [
                ("--upper", {"flag_value": "upper"}),
                ("--lower", {"flag_value": "lower"}),
            ],
            ["--upper"],
            "upper",
            id="non-boolean-no-defaults-cmdline-still-works",
        ),
        # Three-flag string group: explicit default wins from any
        # position in the decorator stack.
        pytest.param(
            [
                ("--upper", {"flag_value": "upper"}),
                ("--mixed", {"flag_value": "MiXeD", "default": "MiXeD"}),
                ("--lower", {"flag_value": "lower"}),
            ],
            [],
            "MiXeD",
            id="three-flags-explicit-default-in-middle",
        ),
        pytest.param(
            [
                ("--upper", {"flag_value": "upper"}),
                ("--lower", {"flag_value": "lower"}),
                ("--default-choice", {"flag_value": "chosen", "default": True}),
            ],
            [],
            "chosen",
            id="three-flags-default-true-substitution-last",
        ),
        # Both options have explicit defaults: last-wins, so the option
        # declared last keeps the slot, regardless of value type.
        pytest.param(
            [
                ("--upper", {"flag_value": "upper", "default": "first"}),
                ("--lower", {"flag_value": "lower", "default": "second"}),
            ],
            [],
            "second",
            id="both-explicit-defaults-string-last-wins",
        ),
        # Mixed boolean and non-boolean ``flag_value`` in the same group
        # is allowed. Both options here carry an explicit default, so last-wins
        # picks the option declared last regardless of value type. The boolean
        # ``default=False`` is a literal value (post-#3239), not a sentinel.
        pytest.param(
            [
                ("--bool-flag", {"flag_value": True, "default": False}),
                ("--str-flag", {"flag_value": "named", "default": "explicit"}),
            ],
            [],
            "explicit",
            id="mixed-bool-and-string-last-wins",
        ),
        pytest.param(
            [
                ("--str-flag", {"flag_value": "named", "default": "explicit"}),
                ("--bool-flag", {"flag_value": True, "default": False}),
            ],
            [],
            False,
            id="mixed-bool-and-string-last-wins-swapped",
        ),
        # Empty string default coexisting with ``default=True``
        # substitution: ``default=""`` is explicit, ``default=True`` is also
        # explicit (and substitutes to the option's ``flag_value``). Last-wins
        # picks the option declared last.
        pytest.param(
            [
                ("--upper", {"flag_value": "upper", "default": True}),
                ("--blank", {"flag_value": "blank", "default": ""}),
            ],
            [],
            "",
            id="default-true-vs-empty-string-last-wins",
        ),
        pytest.param(
            [
                ("--blank", {"flag_value": "blank", "default": ""}),
                ("--upper", {"flag_value": "upper", "default": True}),
            ],
            [],
            "upper",
            id="default-true-vs-empty-string-last-wins-swapped",
        ),
    ],
)
def test_flag_group_competition_non_boolean(runner, opts, args, expected):
    """Same arbitration rules as :func:`test_bool_flag_group_competition`,
    but for feature-switch groups with non-boolean ``flag_value``.
    """

    @click.command()
    def cli(case):
        click.echo(repr(case), nl=False)

    for opt_name, opt_kwargs in opts:
        cli = click.option(opt_name, "case", **opt_kwargs)(cli)

    result = runner.invoke(cli, args)
    assert result.exit_code == 0, result.output
    assert result.output == repr(expected)


@pytest.mark.parametrize(
    ("default_a", "default_b", "args", "expected"),
    [
        # ``default=UNSET`` and an absent ``default`` keyword must produce
        # identical behavior. Both options here are bare boolean flags, so
        # both auto-derive ``False`` and last-wins applies (``--a`` is
        # processed last); the value is ``False`` either way.
        (UNSET, UNSET, [], False),
        # ``default=UNSET`` on one side, explicit on the other: the explicit
        # one wins regardless of decorator order.
        (UNSET, True, [], True),
        (True, UNSET, [], True),
        (UNSET, False, [], False),
        (False, UNSET, [], False),
        # ``default=None`` is a real value, distinct from ``UNSET``, and
        # remains explicit even when the sibling carries an explicit
        # boolean default (3-state).
        (None, UNSET, [], None),
        (UNSET, None, [], None),
        # Explicit ``None`` competing with explicit boolean. The decorator
        # order in this test puts ``--a`` last in ``params``, so the value
        # carried by ``default_a`` wins these "both explicit" ties under
        # last-wins.
        (None, True, [], None),
        (True, None, [], True),
    ],
)
def test_flag_group_unset_vs_none_vs_explicit(
    runner, default_a, default_b, args, expected
):
    """``UNSET`` as an explicit ``default`` must be indistinguishable from
    omitting ``default`` entirely, while ``None`` is a real explicit value.
    """
    a_kwargs = {"flag_value": True}
    if default_a is not UNSET:
        a_kwargs["default"] = default_a
    elif default_a is UNSET:
        # Pass UNSET explicitly to verify it's treated as absent. Skip when
        # the test wants the absent-keyword case (matches default behavior
        # because ``Parameter.__init__`` defaults ``default`` to ``UNSET``).
        a_kwargs["default"] = UNSET

    b_kwargs = {"flag_value": False}
    if default_b is not UNSET:
        b_kwargs["default"] = default_b
    elif default_b is UNSET:
        b_kwargs["default"] = UNSET

    @click.command()
    @click.option("--b", "state", **b_kwargs)
    @click.option("--a", "state", **a_kwargs)
    def cli(state):
        click.echo(repr(state), nl=False)

    result = runner.invoke(cli, args)
    assert result.exit_code == 0, result.output
    assert result.output == repr(expected)


def test_flag_group_competition_duplicate_option_name(runner):
    """The same option name declared twice on the same command is a user
    error.
    """

    @click.command()
    @click.option("--xyz", default="first")
    @click.option("--xyz", default="second")
    def cli(xyz):
        click.echo(repr(xyz), nl=False)

    result = runner.invoke(cli, [])
    assert result.exit_code == 1
    assert isinstance(result.exception, UserWarning)
    assert "used more than once" in str(result.exception)


@pytest.mark.parametrize(
    ("args", "expected"),
    [
        (["--with-xyz", "--with-xyz"], True),
        (["--without-xyz", "--without-xyz"], False),
        (["--with-xyz", "--without-xyz", "--with-xyz"], True),
        (["--without-xyz", "--with-xyz", "--without-xyz"], False),
    ],
)
def test_flag_group_competition_repeated_cmdline(runner, args, expected):
    """Duplicate flags passed in different order to the CLI."""

    @click.command()
    @click.option("--without-xyz", "enable_xyz", flag_value=False)
    @click.option("--with-xyz", "enable_xyz", flag_value=True, default=True)
    def cli(enable_xyz):
        click.echo(repr(enable_xyz), nl=False)

    result = runner.invoke(cli, args)
    assert result.exit_code == 0, result.output
    assert result.output == repr(expected)


@pytest.mark.parametrize(
    ("opts", "args", "expected"),
    [
        pytest.param(
            [
                ("--a", {"flag_value": "a"}),
                ("--b", {"flag_value": "b"}),
                ("--c", {"flag_value": "c"}),
                ("--d", {"flag_value": "d"}),
            ],
            [],
            None,
            id="four-flags-no-defaults-resolves-to-none",
        ),
        pytest.param(
            [
                ("--a", {"flag_value": "a"}),
                ("--b", {"flag_value": "b", "default": "from-b"}),
                ("--c", {"flag_value": "c"}),
                ("--d", {"flag_value": "d"}),
            ],
            [],
            "from-b",
            id="four-flags-only-second-explicit-wins",
        ),
        pytest.param(
            [
                ("--a", {"flag_value": "a"}),
                ("--b", {"flag_value": "b", "default": "from-b"}),
                ("--c", {"flag_value": "c"}),
                ("--d", {"flag_value": "d", "default": "from-d"}),
            ],
            [],
            "from-d",
            id="four-flags-two-explicit-last-wins",
        ),
        pytest.param(
            [
                ("--a", {"flag_value": "a"}),
                ("--b", {"flag_value": "b"}),
                ("--c", {"flag_value": "c"}),
                ("--d", {"flag_value": "d"}),
            ],
            ["--c"],
            "c",
            id="four-flags-cmdline-beats-everything",
        ),
    ],
)
def test_flag_group_competition_four_flags(runner, opts, args, expected):
    """Arbitration rules applies to groups of any size."""

    @click.command()
    def cli(case):
        click.echo(repr(case), nl=False)

    for opt_name, opt_kwargs in opts:
        cli = click.option(opt_name, "case", **opt_kwargs)(cli)

    result = runner.invoke(cli, args)
    assert result.exit_code == 0, result.output
    assert result.output == repr(expected)


@pytest.mark.parametrize(
    ("env", "default_map", "args", "expected"),
    [
        # ``auto_envvar_prefix`` produces an ``ENVIRONMENT`` source through a
        # different code path than an explicit ``envvar=`` keyword. It still
        # must beat any sibling default and be beaten by command-line input.
        pytest.param(
            {"AUTO_ENABLE_XYZ": "1"},
            None,
            [],
            True,
            id="auto-envvar-prefix-beats-default",
        ),
        pytest.param(
            {"AUTO_ENABLE_XYZ": "1"},
            None,
            ["--without-xyz"],
            False,
            id="auto-envvar-prefix-loses-to-cmdline",
        ),
        # ``Sentinel.UNSET`` in ``default_map`` must be skipped (#3224
        # carve-out): the lookup falls through to the parameter default.
        # Inside a feature switch group, the explicit ``default=True`` on
        # ``--with-xyz`` then wins over the sibling's auto-``False``.
        pytest.param(
            {},
            {"enable_xyz": UNSET},
            [],
            True,
            id="unset-default-map-falls-through-to-explicit-default",
        ),
        pytest.param(
            {},
            {"enable_xyz": False},
            [],
            False,
            id="real-default-map-beats-explicit-default",
        ),
    ],
)
def test_flag_group_competition_envvar_prefix_and_unset_default_map(
    runner, monkeypatch, env, default_map, args, expected
):
    for name, value in env.items():
        monkeypatch.setenv(name, value)

    @click.command()
    @click.option("--without-xyz", "enable_xyz", flag_value=False)
    @click.option("--with-xyz", "enable_xyz", flag_value=True, default=True)
    def cli(enable_xyz):
        click.echo(repr(enable_xyz), nl=False)

    invoke_kwargs = {"auto_envvar_prefix": "AUTO"}
    if default_map is not None:
        invoke_kwargs["default_map"] = default_map

    result = runner.invoke(cli, args, **invoke_kwargs)
    assert result.exit_code == 0, result.output
    assert result.output == repr(expected)


@pytest.mark.parametrize(
    ("flag_type", "args", "expect_output"),
    [
        (str, [], "Default\n"),
        (str, ["--theflag"], "FlagValue\n"),
        (str, ["--theflag", "value"], "value\n"),
        (int, [], "0\n"),
        (int, ["--theflag"], "1\n"),
        (int, ["--theflag", "2"], "2\n"),
    ],
)
def test_flag_value_on_option_with_zero_or_one_args(flag_type, args, expect_output):
    """An option with flag_value and is_flag=False can be
    omitted or used with 0 or 1 args.

    Regression test for https://github.com/pallets/click/issues/3084
    """
    if flag_type is str:
        flagopt = click.option(
            "--theflag",
            type=str,
            is_flag=False,
            flag_value="FlagValue",
            default="Default",
        )
    elif flag_type is int:
        flagopt = click.option(
            "--theflag", type=int, is_flag=False, flag_value=1, default=0
        )
    else:
        raise NotImplementedError(flag_type)

    @click.command()
    @flagopt
    def cmd(theflag):
        click.echo(theflag)

    runner = CliRunner()
    result = runner.invoke(cmd, args)
    assert result.exit_code == 0
    assert result.output == expect_output
