import os

import pytest

import click
from click import UNPROCESSED
from click._utils import UNSET
from click.core import ParameterSource


@pytest.mark.parametrize(
    ("default", "type", "expected_output", "expected_type"),
    [
        (42, click.FLOAT, "42.0", float),
        ("42", click.INT, "42", int),
        (1.5, click.STRING, "1.5", str),
        ("1.5", click.FLOAT, "1.5", float),
        ("true", click.BOOL, "True", bool),
        ("0", click.BOOL, "False", bool),
    ],
)
def test_basic_defaults(runner, default, type, expected_output, expected_type):
    """Smoke test: a single option's default is type-coerced.

    This covers basic single-option default type coercion.
    """

    @click.command()
    @click.option("--foo", default=default, type=type)
    def cli(foo):
        assert isinstance(foo, expected_type)
        click.echo(f"FOO:[{foo}]")

    result = runner.invoke(cli, [])
    assert not result.exception
    assert f"FOO:[{expected_output}]" in result.output


def test_multiple_defaults(runner):
    """Smoke test: each element in a multiple-option default is type-coerced.

    .. hint::
        ``test_options.py::test_good_defaults_for_multiple``
        covers the structural default processing (``list`` to
        ``tuple``, various ``nargs``) exhaustively.

        This test fills the gap of explicit
        ``type=click.FLOAT`` coercion on the elements.
    """

    @click.command()
    @click.option("--foo", default=[23, 42], type=click.FLOAT, multiple=True)
    def cli(foo):
        for item in foo:
            assert isinstance(item, float)
            click.echo(item)

    result = runner.invoke(cli, [])
    assert not result.exception
    assert result.output.splitlines() == ["23.0", "42.0"]


def test_nargs_plus_multiple(runner):
    """Smoke test: option with ``nargs=2`` + ``multiple=True`` and a
    tuple-of-tuples default.

    .. hint::
        ``test_options.py::test_good_defaults_for_multiple``
        expands this with many more edge cases with various
        ``nargs``/``multiple``/``default`` combinations.

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

    @click.command()
    @click.option(
        "--arg", default=((1, 2), (3, 4)), nargs=2, multiple=True, type=click.INT
    )
    def cli(arg):
        for a, b in arg:
            click.echo(f"<{a:d}|{b:d}>")

    result = runner.invoke(cli, [])
    assert not result.exception
    assert result.output.splitlines() == ["<1|2>", "<3|4>"]


def test_multiple_flag_default(runner):
    """Default for flags when multiple=True should be empty tuple."""

    @click.command
    # flag due to secondary token
    @click.option("-y/-n", multiple=True)
    # flag due to is_flag
    @click.option("-f", is_flag=True, multiple=True)
    # flag due to flag_value
    @click.option("-v", "v", flag_value=1, multiple=True)
    @click.option("-q", "v", flag_value=-1, multiple=True)
    def cli(y, f, v):
        return y, f, v

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

    result = runner.invoke(cli, ["-y", "-n", "-f", "-v", "-q"], standalone_mode=False)
    assert result.return_value == ((True, False), (True,), (1, -1))


def test_flag_default_map(runner):
    """test flag with default map"""

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

    @cli.command()
    @click.option("--name/--no-name", is_flag=True, show_default=True, help="name flag")
    def foo(name):
        click.echo(name)

    result = runner.invoke(cli, ["foo"])
    assert "False" in result.output

    result = runner.invoke(cli, ["foo", "--help"])
    assert "default: no-name" in result.output

    result = runner.invoke(cli, ["foo"], default_map={"foo": {"name": True}})
    assert "True" in result.output

    result = runner.invoke(cli, ["foo", "--help"], default_map={"foo": {"name": True}})
    assert "default: name" in result.output


def test_shared_param_prefers_first_default(runner):
    """The first ``default=True`` wins when multiple ``flag_value`` options share
    a parameter name, regardless of which positional option carries it.

    .. hint::
        ``test_basic.py::test_flag_value_dual_options`` and
        ``test_options.py::test_default_dual_option_callback`` are wider
        parametrized sibling tests covering many more default-value types (``None``,
        ``UNSET``, strings, numbers) but always place the default on the first
        option. This test complements them by exercising both placements.
    """

    @click.command
    @click.option("--red", "color", flag_value="red")
    @click.option("--green", "color", flag_value="green", default=True)
    def prefers_green(color):
        click.echo(color)

    @click.command
    @click.option("--red", "color", flag_value="red", default=True)
    @click.option("--green", "color", flag_value="green")
    def prefers_red(color):
        click.echo(color)

    result = runner.invoke(prefers_green, [])
    assert "green" in result.output
    result = runner.invoke(prefers_green, ["--red"])
    assert "red" in result.output

    result = runner.invoke(prefers_red, [])
    assert "red" in result.output
    result = runner.invoke(prefers_red, ["--green"])
    assert "green" in result.output


@pytest.mark.parametrize(
    ("default_map", "key", "expected"),
    [
        # Key present in default_map.
        ({"email": "a@b.com"}, "email", "a@b.com"),
        # Key missing from default_map.
        ({"email": "a@b.com"}, "nonexistent", None),
        # No default_map at all / empty default_map.
        (None, "anything", None),
        ({}, "anything", None),
        # Falsy values are returned as-is.
        ({"key": None}, "key", None),
        ({"key": 0}, "key", 0),
        ({"key": ""}, "key", ""),
        ({"key": False}, "key", False),
    ],
)
def test_lookup_default_returns_hides_sentinel(default_map, key, expected):
    """``lookup_default()`` should return ``None`` for missing keys, not :attr:`UNSET`.

    Regression test for https://github.com/pallets/click/issues/3145.
    """
    cmd = click.Command("test")
    ctx = click.Context(cmd)
    if default_map is not None:
        ctx.default_map = default_map
    assert ctx.lookup_default(key) == expected


def test_lookup_default_callable_in_default_map(runner):
    """A callable in ``default_map`` is invoked with ``call=True``
    (the default) and returned as-is with ``call=False``.

    Click uses both paths internally:
    - ``get_default()`` passes ``call=False``,
    - ``resolve_ctx()`` passes ``call=True``.
    """
    factory = lambda: "lazy-value"  # noqa: E731

    # Unit-level: call=True invokes, call=False returns as-is.
    cmd = click.Command("test")
    ctx = click.Context(cmd)
    ctx.default_map = {"name": factory}
    assert ctx.lookup_default("name", call=True) == "lazy-value"
    assert ctx.lookup_default("name", call=False) is factory

    # Integration: the callable is invoked during value resolution.
    @click.command()
    @click.option("--name", default="original", show_default=True)
    @click.pass_context
    def cli(ctx, name):
        click.echo(f"name={name!r}")

    result = runner.invoke(cli, [], default_map={"name": factory})
    assert not result.exception
    assert "name='lazy-value'" in result.output

    # Help rendering gets the callable via call=False, so it
    # shows "(dynamic)" rather than invoking it.
    result = runner.invoke(cli, ["--help"], default_map={"name": factory})
    assert not result.exception
    assert "(dynamic)" in result.output


@pytest.mark.parametrize(
    ("args", "default_map", "expected_value", "expected_source"),
    [
        # CLI arg wins over everything.
        (["--name", "cli"], {"name": "mapped"}, "cli", "COMMANDLINE"),
        # default_map overrides parameter default.
        ([], {"name": "mapped"}, "mapped", "DEFAULT_MAP"),
        # Explicit None in default_map still counts as DEFAULT_MAP.
        ([], {"name": None}, None, "DEFAULT_MAP"),
        # Falsy values in default_map are not confused with missing keys.
        ([], {"name": ""}, "", "DEFAULT_MAP"),
        ([], {"name": 0}, "0", "DEFAULT_MAP"),
        # No default_map falls back to parameter default.
        ([], None, "original", "DEFAULT"),
    ],
)
def test_default_map_source(runner, args, default_map, expected_value, expected_source):
    """``get_parameter_source()`` reports the correct origin for a parameter
    value across the resolution chain: CLI > default_map > parameter default.
    """

    @click.command()
    @click.option("--name", default="original")
    @click.pass_context
    def cli(ctx, name):
        source = ctx.get_parameter_source("name")
        click.echo(f"name={name!r} source={source.name}")

    kwargs = {}
    if default_map is not None:
        kwargs["default_map"] = default_map
    result = runner.invoke(cli, args, **kwargs)
    assert not result.exception
    assert f"name={expected_value!r}" in result.output
    assert f"source={expected_source}" in result.output


def test_parameter_source_during_paramtype_convert(runner):
    """``get_parameter_source()`` is available during ``ParamType.convert``.

    Uses the reproducer from https://github.com/pallets/click/issues/3458.
    """

    class Source(click.ParamType):
        name = "source"

        def convert(self, value, param, ctx):
            return {
                "value": value,
                "source": ctx.get_parameter_source(param.name),
            }

    @click.command()
    @click.option("--default", type=Source(), default="/tmp/file")
    @click.option("--nodefault", type=Source())
    def cli(default, nodefault):
        click.echo(f"default: {default}")
        click.echo(f"nodefault: {nodefault}")

    result = runner.invoke(cli, [])
    assert not result.exception
    assert "default: {'value': '/tmp/file', 'source': " in result.output
    assert "'source': None}" not in result.output.split("default:")[1].split("\n")[0]
    assert (
        result.output == "default: {'value': '/tmp/file', 'source': "
        f"{ParameterSource.DEFAULT!r}}}\nnodefault: None\n"
    )

    result = runner.invoke(cli, ["--default", "cli", "--nodefault", "also"])
    assert not result.exception
    assert (
        "default: {'value': 'cli', 'source': "
        f"{ParameterSource.COMMANDLINE!r}}}" in result.output
    )
    assert (
        "nodefault: {'value': 'also', 'source': "
        f"{ParameterSource.COMMANDLINE!r}}}" in result.output
    )


def test_parameter_source_during_eager_callback(runner):
    """``get_parameter_source()`` is available during eager callbacks.

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

    def eager_cb(ctx, param, value):
        source = ctx.get_parameter_source(param.name)
        click.echo(f"callback source={source.name if source else None}")

    @click.command()
    @click.option(
        "--flag/--no-flag",
        default=False,
        is_eager=True,
        callback=eager_cb,
        expose_value=False,
    )
    def cli():
        source = click.get_current_context().get_parameter_source("flag")
        click.echo(f"final source={source.name}")

    result = runner.invoke(cli, [])
    assert not result.exception
    assert "callback source=DEFAULT" in result.output
    assert "final source=DEFAULT" in result.output

    result = runner.invoke(cli, ["--flag"])
    assert not result.exception
    assert "callback source=COMMANDLINE" in result.output
    assert "final source=COMMANDLINE" in result.output


def test_flask_debug_env_not_stomped_by_default_flag(runner, monkeypatch):
    """Eager callback must not overwrite env when the flag used its default.

    Covers the Flask ``_set_debug`` pattern (pallets/flask#6025). Regression test
    for https://github.com/pallets/click/issues/3458.
    """

    monkeypatch.delenv("APP_DEBUG", raising=False)

    def set_debug(ctx, param, value):
        source = ctx.get_parameter_source(param.name)
        if source is not None and source in (
            ParameterSource.DEFAULT,
            ParameterSource.DEFAULT_MAP,
        ):
            return None
        os.environ["APP_DEBUG"] = "1" if value else "0"
        return value

    @click.command()
    @click.option(
        "--debug/--no-debug",
        default=False,
        is_eager=True,
        expose_value=False,
        callback=set_debug,
    )
    def cli():
        click.echo(f"APP_DEBUG={os.environ.get('APP_DEBUG', '')}")

    monkeypatch.setenv("APP_DEBUG", "1")
    result = runner.invoke(cli, [])
    assert result.exit_code == 0
    assert result.output.strip() == "APP_DEBUG=1"

    result = runner.invoke(cli, ["--debug"])
    assert result.exit_code == 0
    assert result.output.strip() == "APP_DEBUG=1"

    result = runner.invoke(cli, ["--no-debug"])
    assert result.exit_code == 0
    assert result.output.strip() == "APP_DEBUG=0"


def test_parameter_source_on_parse_result_bypass(runner):
    """A losing option keeps its provisional source when ``ctx.params[name]``
    is populated by code that bypassed ``handle_parse_result``.

    This replicate the pattern documented in the "Parameter Modifications" section
    of ``docs/advanced.md``. This test highlight the current behavior of
    ``get_parameter_source()`` but is not intended as a contract enforcement.
    """

    def hijack(ctx, param, value):
        ctx.params["target"] = "hijacked"
        return value

    @click.command()
    @click.option("--hijacker", is_eager=True, callback=hijack, expose_value=False)
    @click.option("--target", default="default_value")
    @click.pass_context
    def cli(ctx, target):
        source = ctx.get_parameter_source("target")
        click.echo(f"value={target} source={source.name if source else 'None'}")

    result = runner.invoke(cli, ["--hijacker", "anything"])
    assert result.exit_code == 0, result.output
    assert "value=hijacked" in result.output
    assert "source=DEFAULT" in result.output


def test_lookup_default_override_respected(runner):
    """A subclass override of ``lookup_default()`` should be called by Click
    internals, not bypassed by a private method.

    Reproduce exactly https://github.com/pallets/click/issues/3145 in which a
    subclass that falls back to prefix-based lookup when the parent returns
    ``None``.

    Previous attempts in https://github.com/pallets/click/pr/3199 were entirely
    bypassing the user's overridden method.
    """

    class CustomContext(click.Context):
        def lookup_default(self, name, call=True):
            default = super().lookup_default(name, call=call)

            if default is not None:
                return default

            # Prefix-based fallback: look up "app" sub-dict for "app_email".
            prefix = name.split("_", 1)[0]
            group = getattr(self, "default_map", None) or {}
            sub = group.get(prefix)
            if isinstance(sub, dict):
                return sub.get(name)
            return default

    @click.command("get-views")
    @click.option("--app-email", default="original", show_default=True)
    @click.pass_context
    def cli(ctx, app_email):
        click.echo(f"app_email={app_email!r}")

    cli.context_class = CustomContext
    default_map = {"app": {"app_email": "prefix@example.com"}}

    # resolve_ctx path: the override provides the runtime value.
    result = runner.invoke(cli, [], default_map=default_map)
    assert not result.exception
    assert "app_email='prefix@example.com'" in result.output

    # get_default path: the override is also used when
    # rendering --help with show_default=True.
    result = runner.invoke(cli, ["--help"], default_map=default_map)
    assert not result.exception
    assert "prefix@example.com" in result.output


class _Marker:
    """Dummy callable used as a flag_value in default tests."""

    pass


@pytest.mark.parametrize(
    ("default_map", "args", "expected"),
    [
        # No default_map: auto-aligned default returns the class, not an instance.
        (None, [], _Marker),
        # CLI flag always returns the class.
        (None, ["--opt"], _Marker),
        # Static value in default_map overrides the auto-aligned flag_value.
        ({"value": "from-map"}, [], "from-map"),
        # Callable in default_map is still invoked (not suppressed by the fix).
        ({"value": lambda: "lazy-map"}, [], "lazy-map"),
        # None in default_map overrides the flag_value.
        ({"value": None}, [], None),
        # CLI arg wins over default_map.
        ({"value": "from-map"}, ["--opt"], _Marker),
    ],
)
def test_default_map_with_callable_flag_value(runner, default_map, args, expected):
    """``default_map`` entries should override the auto-aligned callable ``flag_value``,
    and callable entries in ``default_map`` should still be invoked.

    Verifies the fix for https://github.com/pallets/click/issues/3121 does not
    break ``default_map`` precedence.
    """

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

    kwargs = {}
    if default_map is not None:
        kwargs["default_map"] = default_map
    result = runner.invoke(cli, args, **kwargs)
    assert result.exit_code == 0
    assert result.output == repr(expected)


@pytest.mark.parametrize(
    ("default_map", "option_kwargs", "cli_args", "expected"),
    [
        # String is split for nargs=2 option.
        ({"point": "3 4"}, {"nargs": 2, "type": int}, [], (3, 4)),
        # String is split for explicit Tuple type.
        ({"point": "hello world"}, {"type": (str, str)}, [], ("hello", "world")),
        # Already-structured tuple passes through unchanged.
        ({"point": ("a", "b")}, {"nargs": 2}, [], ("a", "b")),
        # Already-structured list passes through unchanged.
        ({"point": [5, 6]}, {"nargs": 2, "type": int}, [], (5, 6)),
        # CLI args override default_map for nargs > 1.
        (
            {"point": "3 4"},
            {"nargs": 2, "type": int},
            ["--point", "10", "20"],
            (10, 20),
        ),
    ],
)
def test_default_map_nargs(runner, default_map, option_kwargs, cli_args, expected):
    """A string in ``default_map`` for an option with ``nargs > 1`` should be
    split the same way an environment variable string is split.

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

    @click.command()
    @click.option("--point", **option_kwargs)
    def cli(point):
        click.echo(repr(point))

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


def test_unset_in_default_map(runner):
    """An ``UNSET`` value in ``default_map`` should be treated as if
    the key is absent, and so fallback to the parameter's own default.

    Refs: https://github.com/pallets/click/pull/3224#issuecomment-3968643305
    """

    @click.command(
        context_settings={"default_map": {"port": UNSET}},
    )
    @click.option("--port", default=8000)
    def cli(port):
        click.echo(f"port={port}")

    result = runner.invoke(cli, [])
    assert result.exit_code == 0
    assert result.output.strip() == "port=8000"
