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 asyncclick as click
from asyncclick import Option
from asyncclick import UNPROCESSED
from asyncclick._utils import UNSET


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", "(Possible options: --bound, --count)"),
        ("--bount", "(Possible options: --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):
    @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 <asyncclick\.types\.Tuple "
            r"object at 0x[0-9A-Fa-f]+>, but it was 2\.",
        ),
        (
            True,
            2,
            [(1,)],
            ValueError,
            r"'nargs' must be 1 \(or None\) for type <asyncclick\.types\.Tuple "
            r"object at 0x[0-9A-Fa-f]+>, but it was 2\.",
        ),
        (
            True,
            2,
            [(1, 2, 3)],
            ValueError,
            r"'nargs' must be 3 \(or None\) for type <asyncclick\.types\.Tuple "
            r"object at 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 <asyncclick\.types\.Tuple "
            r"object at 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",
        "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"


@pytest.mark.anyio
async def test_callback_validates_prompt(arunner):
    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 = await arunner.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, "{}"),
    ],
)
@pytest.mark.anyio
async 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:
            await argument.process_value(ctx, value)
        assert str(excinfo.value) == "Missing parameter: a"

    else:
        value = await 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 wont see me")
    def cmd(testarg):
        click.echo(testarg)

    result = runner.invoke(cmd)
    assert "I am a default" in result.output
    assert "you wont 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 wont see me")
    def cmd(testoption):
        click.echo(testoption)

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


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 re-use the decorator.
    testoption = click.option("--testoption", cls=CustomOption, help="you wont 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 wont 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_show_default_with_empty_string(runner):
    """When show_default is True and default is set to an empty string."""
    opt = click.Option(["--limit"], default="", show_default=True)
    ctx = click.Context(click.Command("cli"))
    message = opt.get_help_record(ctx)[1]
    assert '[default: ""]' 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


@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.
        ({"type": bool, "default": True, "flag_value": True}, [], True),
        ({"type": bool, "default": True, "flag_value": False}, [], False),
        ({"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
    """

    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 and default to string, so the default value is
        # returned as a string, even if it is a boolean. Also, defaults to the
        # flag_value instead of the default value to support legacy behavior.
        ({"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,
        ),
        # Check that passing exotic flag values like classes is supported, but are
        # rendered to strings when the type is not specified.
        (
            {"flag_value": Class1, "default": True},
            {"flag_value": Class2},
            [],
            re.compile(r"'<test_options.Class1 object at 0x[0-9A-Fa-f]+>'"),
        ),
        (
            {"flag_value": Class1, "default": True},
            {"flag_value": Class2},
            ["--opt1"],
            "<class 'test_options.Class1'>",
        ),
        (
            {"flag_value": Class1, "default": True},
            {"flag_value": Class2},
            ["--opt2"],
            "<class 'test_options.Class2'>",
        ),
        # Even the default is processed as a string.
        ({"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.
        (
            {"flag_value": Class1, "type": UNPROCESSED, "default": True},
            {"flag_value": Class2, "type": UNPROCESSED},
            [],
            re.compile(r"<test_options.Class1 object at 0x[0-9A-Fa-f]+>"),
        ),
        (
            {"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
        # consummd). 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)


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
