import shlex
import sys
from unittest import mock

import pytest

from briefcase import __version__, cmdline
from briefcase.commands import ConvertCommand, DevCommand, NewCommand, UpgradeCommand
from briefcase.console import Console, LogLevel
from briefcase.exceptions import (
    InvalidFormatError,
    InvalidPlatformError,
    NoCommandError,
    UnsupportedCommandError,
)
from briefcase.platforms.linux.system import LinuxSystemCreateCommand
from briefcase.platforms.macOS.app import (
    macOSAppCreateCommand,
    macOSAppPublishCommand,
    macOSAppRunCommand,
)
from briefcase.platforms.web.static import StaticWebDevCommand
from briefcase.platforms.windows.app import WindowsAppCreateCommand


@pytest.fixture
def console() -> Console:
    console = Console()
    yield console
    console.close()


@pytest.fixture
def macOS_console(console, monkeypatch, tmp_path) -> Console:
    # Pretend we're on macOS, regardless of where the tests run.
    monkeypatch.setattr(sys, "platform", "darwin")
    # Force the home directory to a temp path. This is needed because faking macOS
    # causes problems with Platformdirs evaluating the user paths.
    monkeypatch.setenv("BRIEFCASE_HOME", str(tmp_path))

    return console


def do_cmdline_parse(args: str, console: Console):
    """Simulate process to parse command line."""
    Command, extra_cmdline = cmdline.parse_cmdline(shlex.split(args))
    cmd = Command(console=console)
    options, overrides = cmd.parse_options(extra=extra_cmdline)
    return cmd, options, overrides


def test_empty():
    """``briefcase`` returns basic usage."""
    with pytest.raises(NoCommandError, match=r"usage: briefcase") as excinfo:
        cmdline.parse_cmdline([])

    assert excinfo.value.msg.startswith(
        "usage: briefcase [-h] <command> [<platform>] [<format>] ...\n"
        "\n"
        "Briefcase is a tool for converting a Python project into a standalone native\n"
        "application for distribution.\n"
        "\n"
        "Commands:\n"
    )


def test_help_only():
    """``briefcase -h`` returns basic usage."""
    with pytest.raises(NoCommandError, match=r"usage: briefcase") as excinfo:
        cmdline.parse_cmdline(["-h"])

    assert excinfo.value.msg.startswith(
        "usage: briefcase [-h] <command> [<platform>] [<format>] ...\n"
        "\n"
        "Briefcase is a tool for converting a Python project into a standalone native\n"
        "application for distribution.\n"
        "\n"
        "Commands:\n"
    )


def test_version_only(capsys):
    """``briefcase -V`` returns current version."""
    with pytest.raises(SystemExit) as excinfo:
        cmdline.parse_cmdline(["-V"])

    # Normal exit due to displaying help
    assert excinfo.value.code == 0
    # Version is displayed.
    output = capsys.readouterr().out
    assert output == f"{__version__}\n"


def test_unknown_command():
    """``briefcase foobar`` fails as an invalid command."""
    with pytest.raises(SystemExit) as excinfo:
        cmdline.parse_cmdline(["foobar"])

    assert excinfo.value.code == 2
    assert excinfo.value.__context__.argument_name == "command"
    assert excinfo.value.__context__.message.startswith(
        "invalid choice: 'foobar' (choose from"
    )


@pytest.mark.parametrize(
    ("cmdline", "expected_options", "expected_overrides"),
    [
        (
            "new",
            {
                "template": None,
                "template_branch": None,
                "project_overrides": None,
            },
            {},
        ),
        (
            (
                "new --template=path/to/template --template-branch=experiment -C"
                " version=\\'1.2.3\\' -C other=42"
            ),
            {
                "template": "path/to/template",
                "template_branch": "experiment",
                "project_overrides": None,
            },
            {
                "version": "1.2.3",
                "other": 42,
            },
        ),
    ],
)
def test_new_command(macOS_console, cmdline, expected_options, expected_overrides):
    """``briefcase new`` returns the New command."""
    cmd, options, overrides = do_cmdline_parse(cmdline, macOS_console)

    assert isinstance(cmd, NewCommand)
    assert cmd.platform == "all"
    assert cmd.output_format == ""
    assert cmd.console.input_enabled
    assert cmd.console.verbosity == LogLevel.INFO
    assert options == expected_options
    assert overrides == expected_overrides


@pytest.mark.parametrize(
    ("cmdline", "expected_options", "expected_overrides"),
    [
        (
            "convert",
            {
                "template": None,
                "template_branch": None,
                "project_overrides": None,
            },
            {},
        ),
        (
            (
                "convert --template=path/to/template --template-branch=experiment -C"
                " version=\\'1.2.3\\' -C other=42"
            ),
            {
                "template": "path/to/template",
                "template_branch": "experiment",
                "project_overrides": None,
            },
            {
                "version": "1.2.3",
                "other": 42,
            },
        ),
    ],
)
def test_convert_command(console, cmdline, expected_options, expected_overrides):
    """``briefcase convert`` returns the Convert command."""
    cmd, options, overrides = do_cmdline_parse(cmdline, console)

    assert isinstance(cmd, ConvertCommand)
    assert cmd.platform == "all"
    assert cmd.output_format == ""
    assert cmd.console.input_enabled
    assert cmd.console.verbosity == LogLevel.INFO
    assert options == expected_options
    assert overrides == expected_overrides


# Common tests for dev and run commands.
def dev_run_parameters(command):
    return [
        (f"{command} {args}", expected, overrides)
        for args, expected, overrides in [
            ("", {}, {}),
            ("-r", {"update_requirements": True}, {}),
            (
                "-r -C version=\\'1.2.3\\' -C other=42",
                {"update_requirements": True},
                {
                    "version": "1.2.3",
                    "other": 42,
                },
            ),
            ("--update-requirements", {"update_requirements": True}, {}),
            ("--test", {"test_mode": True}, {}),
            ("--test -r", {"test_mode": True, "update_requirements": True}, {}),
            ("--", {}, {}),
            ("-- ''", {"passthrough": [""]}, {}),
            ("-- --test", {"passthrough": ["--test"]}, {}),
            ("--test -- --test", {"test_mode": True, "passthrough": ["--test"]}, {}),
            ("--test -- -r", {"test_mode": True, "passthrough": ["-r"]}, {}),
            (
                "-r -- --test",
                {"update_requirements": True, "passthrough": ["--test"]},
                {},
            ),
            ("-- -y --no maybe", {"passthrough": ["-y", "--no", "maybe"]}, {}),
            (
                "--test -- -y --no maybe",
                {"test_mode": True, "passthrough": ["-y", "--no", "maybe"]},
                {},
            ),
        ]
    ]


@pytest.mark.parametrize(
    ("cmdline", "expected_output_format", "expected_options", "expected_overrides"),
    [
        *[(c, "app", o, ov) for c, o, ov in dev_run_parameters("dev")],
        ("dev --no-run", "app", {"run_app": False}, {}),
        ("dev --no-isolation", "app", {"isolated": False}, {}),
        ("dev macOS", "app", {}, {}),
        ("dev macOS Xcode", "Xcode", {}, {}),
    ],
)
def test_dev_command(
    macOS_console,
    cmdline,
    expected_output_format,
    expected_options,
    expected_overrides,
):
    """``briefcase dev`` returns the Dev command."""
    cmd, options, overrides = do_cmdline_parse(cmdline, macOS_console)

    assert isinstance(cmd, DevCommand)
    assert cmd.platform == "macOS"
    assert cmd.output_format == expected_output_format
    assert cmd.console.input_enabled
    assert cmd.console.verbosity == LogLevel.INFO
    assert options == {
        "appname": None,
        "isolated": True,
        "update_requirements": False,
        "run_app": True,
        "test_mode": False,
        "passthrough": [],
        **expected_options,
    }
    assert overrides == expected_overrides


@pytest.mark.parametrize(
    ("cmdline", "expected_output_format", "expected_options", "expected_overrides"),
    [
        ("dev web", "static", {}, {}),
        ("dev web --no-isolation", "static", {"isolated": False}, {}),
    ],
)
def test_web_dev_command(
    macOS_console,
    cmdline,
    expected_output_format,
    expected_options,
    expected_overrides,
):
    """``briefcase dev`` returns the Dev command."""
    cmd, options, overrides = do_cmdline_parse(cmdline, macOS_console)

    assert isinstance(cmd, StaticWebDevCommand)
    assert cmd.platform == "web"
    assert cmd.output_format == expected_output_format
    assert cmd.console.input_enabled
    assert cmd.console.verbosity == LogLevel.INFO
    assert options == {
        "appname": None,
        "update_requirements": False,
        "run_app": True,
        "test_mode": False,
        "passthrough": [],
        "isolated": True,
        **expected_options,
    }
    assert overrides == expected_overrides


@pytest.mark.parametrize(
    ("cmdline", "expected_options", "expected_overrides"),
    [
        *dev_run_parameters("run"),
        ("run -u", {"update": True}, {}),
        ("run --update", {"update": True}, {}),
        ("run --update-resources", {"update_resources": True}, {}),
        ("run --update-support", {"update_support": True}, {}),
        ("run --update-stub", {"update_stub": True}, {}),
        ("run --no-update", {"no_update": True}, {}),
    ],
)
def test_run_command(
    macOS_console,
    cmdline,
    expected_options,
    expected_overrides,
):
    """``briefcase run`` returns the Run command for the correct platform."""
    cmd, options, overrides = do_cmdline_parse(cmdline, macOS_console)

    assert isinstance(cmd, macOSAppRunCommand)
    assert cmd.platform == "macOS"
    assert cmd.output_format == "app"
    assert cmd.console.input_enabled
    assert cmd.console.verbosity == LogLevel.INFO
    assert options == {
        "appname": None,
        "update": False,
        "update_requirements": False,
        "update_resources": False,
        "update_support": False,
        "update_stub": False,
        "no_update": False,
        "test_mode": False,
        "debugger": None,
        "debugger_host": "localhost",
        "debugger_port": 5678,
        "passthrough": [],
        **expected_options,
    }
    assert overrides == expected_overrides


@pytest.mark.parametrize(
    ("cmdline", "expected_options", "expected_overrides"),
    [
        (
            "upgrade",
            {
                "list_tools": False,
                "tool_list": [],
            },
            {},
        ),
        (
            "upgrade -C version=\"'1.2.3'\" -C other=42",
            {
                "list_tools": False,
                "tool_list": [],
            },
            {
                "version": "1.2.3",
                "other": 42,
            },
        ),
    ],
)
def test_upgrade_command(
    macOS_console,
    cmdline,
    expected_options,
    expected_overrides,
):
    """``briefcase upgrade`` returns the upgrade command."""
    cmd, options, overrides = do_cmdline_parse(cmdline, macOS_console)

    assert isinstance(cmd, UpgradeCommand)
    assert cmd.platform == "macOS"
    assert cmd.output_format == ""
    assert cmd.console.input_enabled
    assert cmd.console.verbosity == LogLevel.INFO
    assert options == expected_options
    assert overrides == expected_overrides


def test_bare_command(macOS_console):
    """``briefcase create`` returns the macOS create app command."""
    cmd, options, overrides = do_cmdline_parse("create", macOS_console)

    assert isinstance(cmd, macOSAppCreateCommand)
    assert cmd.platform == "macOS"
    assert cmd.output_format == "app"
    assert cmd.console.input_enabled
    assert cmd.console.verbosity == LogLevel.INFO
    assert options == {}
    assert overrides == {}


@pytest.mark.skipif(sys.platform != "linux", reason="requires Linux")
def test_linux_default(console):
    """``briefcase create`` returns the linux create system command on Linux."""

    cmd, options, _overrides = do_cmdline_parse("create", console)

    assert isinstance(cmd, LinuxSystemCreateCommand)
    assert cmd.platform == "linux"
    assert cmd.output_format == "system"
    assert cmd.console.input_enabled
    assert cmd.console.verbosity == LogLevel.INFO
    assert options == {}


@pytest.mark.skipif(sys.platform != "darwin", reason="requires macOS")
def test_macOS_default(console):
    """``briefcase create`` returns the macOS create command on Linux."""

    cmd, options, overrides = do_cmdline_parse("create", console)

    assert isinstance(cmd, macOSAppCreateCommand)
    assert cmd.platform == "macOS"
    assert cmd.output_format == "app"
    assert cmd.console.input_enabled
    assert cmd.console.verbosity == LogLevel.INFO
    assert options == {}
    assert overrides == {}


@pytest.mark.skipif(sys.platform != "win32", reason="requires Windows")
def test_windows_default(console):
    """``briefcase create`` returns the Windows create app command on Windows."""

    cmd, options, overrides = do_cmdline_parse("create", console)

    assert isinstance(cmd, WindowsAppCreateCommand)
    assert cmd.platform == "windows"
    assert cmd.output_format == "app"
    assert cmd.console.input_enabled
    assert cmd.console.verbosity == LogLevel.INFO
    assert options == {}
    assert overrides == {}


def test_bare_command_help(capsys, macOS_console):
    """``briefcase create -h`` returns the macOS create app command help."""
    with pytest.raises(SystemExit) as excinfo:
        do_cmdline_parse("create -h", macOS_console)

    # Normal exit due to displaying help
    assert excinfo.value.code == 0
    # Help message is for default platform and format
    output = capsys.readouterr().out
    assert output.startswith(
        "usage: briefcase create macOS app [-h] [-C KEY=VALUE] [-v] [-V] [--no-input]\n"
        "                                  [--log] [-a APP_NAME]\n"
        "\n"
        "Create and populate a macOS app.\n"
    )


def test_bare_command_version(capsys, console):
    """``briefcase create -V`` returns the version."""
    with pytest.raises(SystemExit) as excinfo:
        do_cmdline_parse("create -V", console)

    # Normal exit due to displaying help
    assert excinfo.value.code == 0
    # Version is displayed.
    output = capsys.readouterr().out
    assert output == f"{__version__}\n"


def test_command_unknown_platform(macOS_console):
    """``briefcase create foobar`` raises an unknown platform error."""
    expected_exc_regex = r"Invalid platform 'foobar'; \(choose from: .*\)"
    with pytest.raises(InvalidPlatformError, match=expected_exc_regex):
        do_cmdline_parse("create foobar", macOS_console)


def test_command_explicit_platform(macOS_console):
    """``briefcase create linux`` returns linux create app command."""
    cmd, options, overrides = do_cmdline_parse("create linux", macOS_console)

    assert isinstance(cmd, LinuxSystemCreateCommand)
    assert cmd.platform == "linux"
    assert cmd.output_format == "system"
    assert cmd.console.input_enabled
    assert cmd.console.verbosity == LogLevel.INFO
    assert options == {}
    assert overrides == {}


def test_command_explicit_platform_case_handling(macOS_console):
    """``briefcase create macOS`` returns macOS create app command."""
    # This is all lower case; the command normalizes to macOS
    cmd, options, overrides = do_cmdline_parse("create macOS", macOS_console)

    assert isinstance(cmd, macOSAppCreateCommand)
    assert cmd.platform == "macOS"
    assert cmd.output_format == "app"
    assert cmd.console.input_enabled
    assert cmd.console.verbosity == LogLevel.INFO
    assert options == {}
    assert overrides == {}


def test_command_explicit_platform_help(capsys, macOS_console):
    """``briefcase create macOS -h`` returns the macOS create app command help."""
    with pytest.raises(SystemExit) as excinfo:
        do_cmdline_parse("create macOS -h", macOS_console)

    # Normal exit due to displaying help
    assert excinfo.value.code == 0
    # Help message is for default platform and format
    output = capsys.readouterr().out
    assert output.startswith(
        "usage: briefcase create macOS app [-h] [-C KEY=VALUE] [-v] [-V] [--no-input]\n"
        "                                  [--log] [-a APP_NAME]\n"
        "\n"
        "Create and populate a macOS app.\n"
    )


def test_command_explicit_format(macOS_console):
    """``briefcase create macOS app`` returns the macOS create app command."""
    cmd, options, overrides = do_cmdline_parse("create macOS app", macOS_console)

    assert isinstance(cmd, macOSAppCreateCommand)
    assert cmd.platform == "macOS"
    assert cmd.output_format == "app"
    assert cmd.console.input_enabled
    assert cmd.console.verbosity == LogLevel.INFO
    assert options == {}
    assert overrides == {}


def test_command_unknown_format(macOS_console):
    """``briefcase create macOS foobar`` returns an invalid format error."""
    expected_exc_regex = r"Invalid format 'foobar'; \(choose from: app, Xcode\)"
    with pytest.raises(InvalidFormatError, match=expected_exc_regex):
        do_cmdline_parse("create macOS foobar", macOS_console)


def test_command_explicit_unsupported_format(monkeypatch, macOS_console):
    """``briefcase create macOS homebrew`` raises an error because the format isn't
    supported (yet)"""
    # Mock the output formats to include a "homebrew" backend with no commands.
    monkeypatch.setattr(
        cmdline,
        "get_output_formats",
        mock.MagicMock(return_value={"homebrew": None}),
    )

    with pytest.raises(
        UnsupportedCommandError,
        match=(
            r"The create command for the macOS homebrew format has not been implemented"
            r" \(yet!\)."
        ),
    ):
        do_cmdline_parse("create macOS homebrew", macOS_console)


def test_command_explicit_format_help(capsys, macOS_console):
    """``briefcase create macOS app -h`` returns the macOS create app help."""
    with pytest.raises(SystemExit) as excinfo:
        do_cmdline_parse("create macOS app -h", macOS_console)

    # Normal exit due to displaying help
    assert excinfo.value.code == 0
    # Help message is for default platform, but app format
    output = capsys.readouterr().out
    assert output.startswith(
        "usage: briefcase create macOS app [-h] [-C KEY=VALUE] [-v] [-V] [--no-input]\n"
        "                                  [--log] [-a APP_NAME]\n"
        "\n"
        "Create and populate a macOS app.\n"
    )


def test_command_disable_input(macOS_console):
    """``briefcase create --no-input`` disables console input."""
    cmd, options, overrides = do_cmdline_parse("create --no-input", macOS_console)

    assert isinstance(cmd, macOSAppCreateCommand)
    assert cmd.platform == "macOS"
    assert cmd.output_format == "app"
    assert not cmd.console.input_enabled
    assert cmd.console.verbosity == LogLevel.INFO
    assert options == {}
    assert overrides == {}


def test_command_options(capsys, macOS_console):
    """Commands can provide their own arguments."""
    # Invoke a command that is known to have its own custom arguments
    # (In this case, the channel argument for publication)
    cmd, options, overrides = do_cmdline_parse("publish macos app -c s3", macOS_console)

    assert isinstance(cmd, macOSAppPublishCommand)
    assert cmd.console.input_enabled
    assert cmd.console.verbosity == LogLevel.INFO
    assert options == {"update": False, "packaging_format": "app", "channel": "s3"}
    assert overrides == {}


def test_command_overrides(capsys, macOS_console):
    """Configuration overrides can be specified."""
    # Invoke a command that is known to have its own custom arguments
    # (In this case, the channel argument for publication)
    cmd, options, overrides = do_cmdline_parse(
        "publish macos app -C version=\"'1.2.3'\" -C extra=42", macOS_console
    )

    assert isinstance(cmd, macOSAppPublishCommand)
    assert cmd.console.input_enabled
    assert cmd.console.verbosity == LogLevel.INFO
    assert options == {"update": False, "packaging_format": "app", "channel": None}
    assert overrides == {
        "version": "1.2.3",
        "extra": 42,
    }


def test_unknown_command_options(capsys, macOS_console):
    """Commands can provide their own arguments."""
    # Invoke a command but provide an option. that isn't defined
    with pytest.raises(SystemExit) as excinfo:
        do_cmdline_parse("publish macOS xcode -x foobar", macOS_console)

    # Normal exit due to displaying help
    assert excinfo.value.code == 2
    # Help message is for default platform and format
    output = capsys.readouterr().err

    assert output.startswith(
        "usage: briefcase publish macOS Xcode [-h] [-C KEY=VALUE] [-v] [-V]\n"
        "                                     [--no-input] [--log] [-a APP_NAME] [-u]\n"
        "                                     [-p {Xcode}] [-c CHANNEL]\n"
        "briefcase publish macOS Xcode: error: unrecognized arguments: -x foobar"
    )


@pytest.mark.parametrize(
    "cmdline",
    [
        "dev iOS",
        "dev Android",
    ],
)
def test_dev_command_unsupported_platform(cmdline, console):
    """Unsupported platforms should raise an UnsupportedCommandError."""

    with pytest.raises(UnsupportedCommandError) as e:
        do_cmdline_parse(cmdline, console)

    msg = str(e.value)
    assert "dev command for the" in msg
    assert "not been implemented" in msg
