from types import SimpleNamespace

import click
from click.testing import CliRunner

from mvt.cli import cli as mvt_cli
from mvt.common.cli_plugins import (
    ANDROID_CLI_PLUGIN_GROUP,
    IOS_CLI_PLUGIN_GROUP,
    MVT_CUSTOM_COMMANDS_ENV,
    NEUTRAL_CLI_PLUGIN_GROUP,
    BrokenPluginCommand,
    load_cli_commands_option,
    register_cli_commands_from_path,
    register_cli_plugins,
    register_installed_cli_commands,
)


# Keep the banner of the mvt group callback from checking for updates online.
OFFLINE = ["--disable-update-check", "--disable-indicator-update-check"]

COMMAND_TEMPLATE = """
import click


@click.command({name!r})
@click.pass_context
def cli(ctx):
    click.echo({message!r})
    if ctx.obj:
        click.echo(ctx.obj.get("marker", ""))
"""


def _write_command(path, name, message="command ran"):
    path.write_text(
        COMMAND_TEMPLATE.format(name=name, message=message),
        encoding="utf-8",
    )
    return path


def _make_group():
    @click.group()
    @load_cli_commands_option
    @click.pass_context
    def group(ctx):
        ctx.ensure_object(dict)
        ctx.obj["marker"] = "parent context"

    return group


def _entry_point(name, value, command=None, exception=None, distribution="plugin"):
    def load():
        if exception is not None:
            raise exception
        return command

    dist = SimpleNamespace(metadata={"Name": distribution}, version="1.0")
    return SimpleNamespace(name=name, value=value, load=load, dist=dist)


def test_load_command_option_registers_command_before_resolution(tmp_path):
    command_path = _write_command(tmp_path / "hello.py", "hello")
    group = _make_group()

    result = CliRunner().invoke(
        group,
        ["--load-command", str(command_path), "hello"],
    )

    assert result.exit_code == 0
    assert "command ran" in result.output
    assert "parent context" in result.output


def test_load_command_option_supports_folders_and_repeated_paths(tmp_path):
    folder = tmp_path / "commands"
    folder.mkdir()
    _write_command(folder / "b.py", "second")
    _write_command(folder / "a.py", "first")
    _write_command(folder / ".hidden.py", "hidden")
    _write_command(folder / "__init__.py", "init")
    other = _write_command(tmp_path / "third.py", "third")
    group = _make_group()

    result = CliRunner().invoke(
        group,
        [
            "--load-command",
            str(folder),
            "--load-command",
            str(other),
            "--load-command",
            str(other),
            "--help",
        ],
    )

    assert result.exit_code == 0
    assert "first" in result.output
    assert "second" in result.output
    assert "third" in result.output
    assert "hidden" not in result.output
    assert "init" not in result.output


def test_loaded_command_participates_in_shell_completion(tmp_path):
    command_path = _write_command(tmp_path / "hello.py", "hello")
    group = _make_group()
    words = f"group --load-command {command_path} he"

    result = CliRunner().invoke(
        group,
        [],
        env={
            "_GROUP_COMPLETE": "bash_complete",
            "COMP_WORDS": words,
            "COMP_CWORD": "3",
        },
    )

    assert result.exit_code == 0
    assert "plain,hello" in result.output


def test_explicit_command_import_and_contract_failures_are_usage_errors(tmp_path):
    broken_path = tmp_path / "broken.py"
    broken_path.write_text("raise RuntimeError('broken import')", encoding="utf-8")
    missing_cli_path = tmp_path / "missing_cli.py"
    missing_cli_path.write_text("value = 1", encoding="utf-8")

    broken_result = CliRunner().invoke(
        _make_group(),
        ["--load-command", str(broken_path), "broken"],
    )
    missing_cli_result = CliRunner().invoke(
        _make_group(),
        ["--load-command", str(missing_cli_path), "missing-cli"],
    )

    assert broken_result.exit_code == 2
    assert "broken import" in broken_result.output
    assert missing_cli_result.exit_code == 2
    assert "must export a Click command or group named 'cli'" in (
        missing_cli_result.output
    )


def test_environment_command_failure_gets_broken_placeholder(tmp_path):
    command_path = tmp_path / "broken_command.py"
    command_path.write_text("raise RuntimeError('broken import')", encoding="utf-8")
    group = click.Group()

    registered = register_cli_commands_from_path(group, command_path)

    assert registered == ["broken-command"]
    assert isinstance(group.commands["broken-command"], BrokenPluginCommand)
    result = CliRunner().invoke(group, ["broken-command"])
    assert result.exit_code == 1
    assert "broken import" in result.output
    assert str(command_path) in result.output


def test_environment_command_system_exit_gets_broken_placeholder(tmp_path):
    command_path = tmp_path / "exiting_command.py"
    command_path.write_text("raise SystemExit(7)", encoding="utf-8")
    group = click.Group()

    registered = register_cli_commands_from_path(group, command_path)

    assert registered == ["exiting-command"]
    assert isinstance(group.commands["exiting-command"], BrokenPluginCommand)
    result = CliRunner().invoke(group, ["exiting-command"])
    assert result.exit_code == 1
    assert "Unable to import custom command" in result.output
    assert result.output.rstrip().endswith(": 7")


def test_installed_entry_point_name_is_the_command_name(monkeypatch):
    @click.command("internal-name")
    def command():
        click.echo("installed command ran")

    entry_point = _entry_point(
        "external-name",
        "example_plugin:cli",
        command=command,
    )
    monkeypatch.setattr(
        "mvt.common.cli_plugins.importlib.metadata.entry_points",
        lambda **kwargs: [entry_point],
    )
    group = click.Group()

    registered = register_installed_cli_commands(group, IOS_CLI_PLUGIN_GROUP)

    assert registered == ["external-name"]
    assert "internal-name" not in group.commands
    result = CliRunner().invoke(group, ["external-name"])
    assert result.exit_code == 0
    assert result.output == "installed command ran\n"


def test_broken_installed_plugin_does_not_break_cli(monkeypatch):
    broken = _entry_point(
        "broken",
        "broken_plugin:cli",
        exception=RuntimeError("missing dependency"),
        distribution="broken-plugin",
    )
    monkeypatch.setattr(
        "mvt.common.cli_plugins.importlib.metadata.entry_points",
        lambda **kwargs: [broken],
    )
    group = click.Group()

    register_installed_cli_commands(group, IOS_CLI_PLUGIN_GROUP)

    help_result = CliRunner().invoke(group, ["--help"])
    assert help_result.exit_code == 0
    assert "Warning: external command could not be loaded." in help_result.output

    result = CliRunner().invoke(group, ["broken"])
    assert result.exit_code == 1
    assert "broken-plugin 1.0 (broken_plugin:cli)" in result.output
    assert "RuntimeError: missing dependency" in result.output


def test_installed_plugin_system_exit_does_not_break_cli(monkeypatch):
    exiting = _entry_point(
        "exiting",
        "exiting_plugin:cli",
        exception=SystemExit(7),
        distribution="exiting-plugin",
    )
    monkeypatch.setattr(
        "mvt.common.cli_plugins.importlib.metadata.entry_points",
        lambda **kwargs: [exiting],
    )
    group = click.Group()

    register_installed_cli_commands(group, IOS_CLI_PLUGIN_GROUP)

    help_result = CliRunner().invoke(group, ["--help"])
    assert help_result.exit_code == 0
    result = CliRunner().invoke(group, ["exiting"])
    assert result.exit_code == 1
    assert "SystemExit: 7" in result.output


def test_non_click_entry_point_gets_broken_placeholder(monkeypatch):
    invalid = _entry_point("invalid", "plugin:value", command=object())
    monkeypatch.setattr(
        "mvt.common.cli_plugins.importlib.metadata.entry_points",
        lambda **kwargs: [invalid],
    )
    group = click.Group()

    register_installed_cli_commands(group, IOS_CLI_PLUGIN_GROUP)

    assert isinstance(group.commands["invalid"], BrokenPluginCommand)
    result = CliRunner().invoke(group, ["invalid"])
    assert result.exit_code == 1
    assert "must resolve to a Click command or group" in result.output


def test_entry_point_discovery_failure_does_not_break_group(monkeypatch, caplog):
    def fail_discovery(**kwargs):
        raise RuntimeError("invalid package metadata")

    monkeypatch.setattr(
        "mvt.common.cli_plugins.importlib.metadata.entry_points",
        fail_discovery,
    )
    group = click.Group()

    registered = register_installed_cli_commands(group, IOS_CLI_PLUGIN_GROUP)

    assert registered == []
    assert not group.commands
    assert "Unable to discover external commands" in caplog.text
    assert "invalid package metadata" in caplog.text


def test_builtin_and_first_external_command_win_collisions(monkeypatch, caplog):
    @click.command("version")
    def core_version():
        pass

    @click.command()
    def first():
        pass

    @click.command()
    def second():
        pass

    entry_points = [
        _entry_point("duplicate", "z_plugin:cli", command=second, distribution="z"),
        _entry_point("version", "plugin:version", command=first),
        _entry_point("duplicate", "a_plugin:cli", command=first, distribution="a"),
    ]
    monkeypatch.setattr(
        "mvt.common.cli_plugins.importlib.metadata.entry_points",
        lambda **kwargs: entry_points,
    )
    group = click.Group(commands={"version": core_version})

    registered = register_installed_cli_commands(group, IOS_CLI_PLUGIN_GROUP)

    assert registered == ["duplicate"]
    assert group.commands["version"] is core_version
    assert group.commands["duplicate"] is first
    assert "the command name is already registered" in caplog.text


def test_explicit_command_cannot_replace_existing_command(tmp_path):
    command_path = _write_command(tmp_path / "version.py", "version")
    group = _make_group()

    @group.command("version")
    def core_version():
        pass

    result = CliRunner().invoke(
        group,
        ["--load-command", str(command_path), "version"],
    )

    assert result.exit_code == 2
    assert "the command name is already registered" in result.output


def test_platform_entry_point_groups_and_environment_paths_are_separate(
    tmp_path, monkeypatch
):
    ios_path = _write_command(tmp_path / "ios.py", "ios-file")
    android_path = _write_command(tmp_path / "android.py", "android-file")

    @click.command()
    def ios_package():
        pass

    @click.command()
    def android_package():
        pass

    def entry_points(*, group):
        if group == IOS_CLI_PLUGIN_GROUP:
            return [_entry_point("ios-package", "ios_plugin:cli", ios_package)]
        if group == ANDROID_CLI_PLUGIN_GROUP:
            return [
                _entry_point("android-package", "android_plugin:cli", android_package)
            ]
        return []

    monkeypatch.setattr(
        "mvt.common.cli_plugins.importlib.metadata.entry_points",
        entry_points,
    )
    monkeypatch.setenv("TEST_IOS_COMMANDS", str(ios_path))
    monkeypatch.setenv("TEST_ANDROID_COMMANDS", str(android_path))
    ios_group = click.Group()
    android_group = click.Group()

    register_cli_plugins(
        ios_group,
        entry_point_group=IOS_CLI_PLUGIN_GROUP,
        environment_variable="TEST_IOS_COMMANDS",
    )
    register_cli_plugins(
        android_group,
        entry_point_group=ANDROID_CLI_PLUGIN_GROUP,
        environment_variable="TEST_ANDROID_COMMANDS",
    )

    assert set(ios_group.commands) == {"ios-file", "ios-package"}
    assert set(android_group.commands) == {"android-file", "android-package"}


def test_neutral_entry_point_group_is_not_registered_on_the_platform_clis(
    monkeypatch,
):
    @click.command()
    def neutral_package():
        pass

    def entry_points(*, group):
        if group == NEUTRAL_CLI_PLUGIN_GROUP:
            return [
                _entry_point("neutral-package", "neutral_plugin:cli", neutral_package)
            ]
        return []

    monkeypatch.setattr(
        "mvt.common.cli_plugins.importlib.metadata.entry_points",
        entry_points,
    )
    ios_group = click.Group()
    android_group = click.Group()

    register_cli_plugins(
        ios_group,
        entry_point_group=IOS_CLI_PLUGIN_GROUP,
        environment_variable="TEST_IOS_COMMANDS",
    )
    register_cli_plugins(
        android_group,
        entry_point_group=ANDROID_CLI_PLUGIN_GROUP,
        environment_variable="TEST_ANDROID_COMMANDS",
    )

    assert not ios_group.commands
    assert not android_group.commands


def test_environment_command_wins_collision_with_installed_command(
    tmp_path, monkeypatch, caplog
):
    command_path = _write_command(
        tmp_path / "duplicate.py",
        "duplicate",
        message="environment command ran",
    )

    @click.command()
    def installed_command():
        pass

    def entry_points(*, group):
        if group == IOS_CLI_PLUGIN_GROUP:
            return [
                _entry_point(
                    "duplicate",
                    "ios_plugin:cli",
                    installed_command,
                    distribution="ios-plugin",
                )
            ]
        return []

    monkeypatch.setattr(
        "mvt.common.cli_plugins.importlib.metadata.entry_points",
        entry_points,
    )
    monkeypatch.setenv("TEST_IOS_COMMANDS", str(command_path))
    group = click.Group()

    register_cli_plugins(
        group,
        entry_point_group=IOS_CLI_PLUGIN_GROUP,
        environment_variable="TEST_IOS_COMMANDS",
    )

    assert group.commands["duplicate"] is not installed_command
    result = CliRunner().invoke(group, ["duplicate"])
    assert result.exit_code == 0
    assert "environment command ran" in result.output
    assert "the command name is already registered" in caplog.text
    assert "ios-plugin 1.0 (ios_plugin:cli)" in caplog.text


def test_the_mvt_cli_gets_the_neutral_commands_and_no_platform_command(
    monkeypatch, restore_cli_commands
):
    @click.command()
    def shared_package():
        click.echo("shared command ran")

    @click.command()
    def ios_package():
        pass

    def entry_points(*, group):
        if group == NEUTRAL_CLI_PLUGIN_GROUP:
            return [_entry_point("shared-package", "shared_plugin:cli", shared_package)]
        if group == IOS_CLI_PLUGIN_GROUP:
            return [_entry_point("ios-package", "ios_plugin:cli", ios_package)]
        return []

    monkeypatch.setattr(
        "mvt.common.cli_plugins.importlib.metadata.entry_points",
        entry_points,
    )

    register_cli_plugins(
        mvt_cli,
        entry_point_group=NEUTRAL_CLI_PLUGIN_GROUP,
        environment_variable=MVT_CUSTOM_COMMANDS_ENV,
    )

    assert "ios-package" not in mvt_cli.commands
    result = CliRunner().invoke(mvt_cli, [*OFFLINE, "shared-package"])
    assert result.exit_code == 0
    assert "shared command ran" in result.output


def test_builtin_mvt_command_wins_collision_with_neutral_command(
    monkeypatch, caplog, restore_cli_commands
):
    @click.command()
    def neutral_version():
        pass

    def entry_points(*, group):
        if group == NEUTRAL_CLI_PLUGIN_GROUP:
            return [
                _entry_point(
                    "version",
                    "neutral_plugin:cli",
                    neutral_version,
                    distribution="neutral-plugin",
                )
            ]
        return []

    monkeypatch.setattr(
        "mvt.common.cli_plugins.importlib.metadata.entry_points",
        entry_points,
    )
    builtin_version = mvt_cli.commands["version"]

    register_cli_plugins(
        mvt_cli,
        entry_point_group=NEUTRAL_CLI_PLUGIN_GROUP,
        environment_variable=MVT_CUSTOM_COMMANDS_ENV,
    )

    assert mvt_cli.commands["version"] is builtin_version
    assert "the command name is already registered" in caplog.text
    assert "neutral-plugin 1.0 (neutral_plugin:cli)" in caplog.text


def test_broken_neutral_plugin_does_not_break_the_mvt_cli(
    monkeypatch, restore_cli_commands
):
    def entry_points(*, group):
        if group == NEUTRAL_CLI_PLUGIN_GROUP:
            return [
                _entry_point(
                    "broken",
                    "broken_plugin:cli",
                    exception=RuntimeError("missing dependency"),
                    distribution="broken-plugin",
                )
            ]
        return []

    monkeypatch.setattr(
        "mvt.common.cli_plugins.importlib.metadata.entry_points",
        entry_points,
    )

    register_cli_plugins(
        mvt_cli,
        entry_point_group=NEUTRAL_CLI_PLUGIN_GROUP,
        environment_variable=MVT_CUSTOM_COMMANDS_ENV,
    )

    assert isinstance(mvt_cli.commands["broken"], BrokenPluginCommand)
    result = CliRunner().invoke(mvt_cli, [*OFFLINE, "version"])
    assert result.exit_code == 0
