import json
import secrets

import pytest

from hatch.errors import PythonDistributionResolutionError
from hatch.python.core import InstalledDistribution
from hatch.python.distributions import ORDERED_DISTRIBUTIONS
from hatch.python.resolve import get_distribution


def test_unknown(hatch, helpers, path_append, mocker):
    install = mocker.patch("hatch.python.core.PythonManager.install")

    result = hatch("python", "install", "foo", "bar")

    assert result.exit_code == 1, result.output
    assert result.output == helpers.dedent(
        """
        Unknown distributions: foo, bar
        """
    )

    install.assert_not_called()
    path_append.assert_not_called()


def test_incompatible_single(hatch, helpers, path_append, dist_name, mocker):
    mocker.patch("hatch.python.resolve.get_distribution", side_effect=PythonDistributionResolutionError)
    install = mocker.patch("hatch.python.core.PythonManager.install")

    result = hatch("python", "install", dist_name)

    assert result.exit_code == 1, result.output
    assert result.output == helpers.dedent(
        f"""
        Incompatible distributions: {dist_name}
        """
    )

    install.assert_not_called()
    path_append.assert_not_called()


def test_incompatible_all(hatch, helpers, path_append, mocker):
    mocker.patch("hatch.python.resolve.get_distribution", side_effect=PythonDistributionResolutionError)
    install = mocker.patch("hatch.python.core.PythonManager.install")

    result = hatch("python", "install", "all")

    assert result.exit_code == 1, result.output
    assert result.output == helpers.dedent(
        f"""
        Incompatible distributions: {", ".join(ORDERED_DISTRIBUTIONS)}
        """
    )

    install.assert_not_called()
    path_append.assert_not_called()


@pytest.mark.requires_internet
def test_installation(
    hatch, helpers, temp_dir_data, platform, path_append, default_shells, compatible_python_distributions
):
    selection = [name for name in compatible_python_distributions if not name.startswith("pypy")]
    dist_name = secrets.choice(selection)
    result = hatch("python", "install", dist_name)

    install_dir = temp_dir_data / "data" / "pythons" / dist_name
    metadata_file = install_dir / InstalledDistribution.metadata_filename()
    python_path = install_dir / json.loads(metadata_file.read_text())["python_path"]

    assert result.exit_code == 0, result.output
    assert result.output == helpers.dedent(
        f"""
        Installing {dist_name}
        Installed {dist_name} @ {install_dir}

        The following directory has been added to your PATH (pending a shell restart):

        {python_path.parent}
        """
    )

    assert python_path.is_file()

    output = platform.check_command_output([python_path, "-c", "import sys;print(sys.executable)"]).strip()
    assert output == str(python_path)

    output = platform.check_command_output([python_path, "--version"]).strip()
    assert output.startswith(f"Python {dist_name}.")

    path_append.assert_called_once_with(str(python_path.parent), shells=default_shells)


def test_already_installed_latest(hatch, helpers, temp_dir_data, path_append, dist_name, mocker):
    install = mocker.patch("hatch.python.core.PythonManager.install")
    install_dir = temp_dir_data / "data" / "pythons"
    installed_dist = helpers.write_distribution(install_dir, dist_name)

    result = hatch("python", "install", dist_name)

    assert result.exit_code == 0, result.output
    assert result.output == helpers.dedent(
        f"""
        The latest version is already installed: {installed_dist.version}
        """
    )

    install.assert_not_called()
    path_append.assert_not_called()


def test_already_installed_update_disabled(hatch, helpers, temp_dir_data, path_append, dist_name, mocker):
    install = mocker.patch("hatch.python.core.PythonManager.install")
    install_dir = temp_dir_data / "data" / "pythons"
    helpers.write_distribution(install_dir, dist_name)
    helpers.downgrade_distribution_metadata(install_dir / dist_name)

    result = hatch("python", "install", dist_name, input="n\n")

    assert result.exit_code == 1, result.output
    assert result.output == helpers.dedent(
        f"""
        Update {dist_name}? [y/N]: n
        Distribution is already installed: {dist_name}
        """
    )

    install.assert_not_called()
    path_append.assert_not_called()


def test_already_installed_update_prompt(hatch, helpers, temp_dir_data, path_append, default_shells, dist_name, mocker):
    install_dir = temp_dir_data / "data" / "pythons"
    helpers.write_distribution(install_dir, dist_name)

    dist_dir = install_dir / dist_name
    metadata = helpers.downgrade_distribution_metadata(dist_dir)
    python_path = dist_dir / metadata["python_path"]
    install = mocker.patch(
        "hatch.python.core.PythonManager.install", return_value=mocker.MagicMock(path=dist_dir, python_path=python_path)
    )

    result = hatch("python", "install", dist_name, input="y\n")

    assert result.exit_code == 0, result.output
    assert result.output == helpers.dedent(
        f"""
        Update {dist_name}? [y/N]: y
        Updating {dist_name}
        Updated {dist_name} @ {dist_dir}

        The following directory has been added to your PATH (pending a shell restart):

        {python_path.parent}
        """
    )

    install.assert_called_once_with(dist_name)
    path_append.assert_called_once_with(str(python_path.parent), shells=default_shells)


def test_already_installed_update_flag(hatch, helpers, temp_dir_data, path_append, default_shells, dist_name, mocker):
    install_dir = temp_dir_data / "data" / "pythons"
    helpers.write_distribution(install_dir, dist_name)

    dist_dir = install_dir / dist_name
    metadata = helpers.downgrade_distribution_metadata(dist_dir)
    python_path = dist_dir / metadata["python_path"]
    install = mocker.patch(
        "hatch.python.core.PythonManager.install", return_value=mocker.MagicMock(path=dist_dir, python_path=python_path)
    )

    result = hatch("python", "install", "--update", dist_name)

    assert result.exit_code == 0, result.output
    assert result.output == helpers.dedent(
        f"""
        Updating {dist_name}
        Updated {dist_name} @ {dist_dir}

        The following directory has been added to your PATH (pending a shell restart):

        {python_path.parent}
        """
    )

    install.assert_called_once_with(dist_name)
    path_append.assert_called_once_with(str(python_path.parent), shells=default_shells)


@pytest.mark.parametrize("detector", ["in_current_path", "in_new_path"])
def test_already_in_path(hatch, helpers, temp_dir_data, path_append, mocker, detector, dist_name):
    mocker.patch(f"userpath.{detector}", return_value=True)
    dist_dir = temp_dir_data / "data" / "pythons" / dist_name
    python_path = dist_dir / get_distribution(dist_name).python_path
    install = mocker.patch(
        "hatch.python.core.PythonManager.install", return_value=mocker.MagicMock(path=dist_dir, python_path=python_path)
    )

    result = hatch("python", "install", dist_name)

    assert result.exit_code == 0, result.output
    assert result.output == helpers.dedent(
        f"""
        Installing {dist_name}
        Installed {dist_name} @ {dist_dir}
        """
    )

    install.assert_called_once_with(dist_name)
    path_append.assert_not_called()


def test_private(hatch, helpers, temp_dir_data, path_append, dist_name, mocker):
    dist_dir = temp_dir_data / "data" / "pythons" / dist_name
    python_path = dist_dir / get_distribution(dist_name).python_path
    install = mocker.patch(
        "hatch.python.core.PythonManager.install", return_value=mocker.MagicMock(path=dist_dir, python_path=python_path)
    )

    result = hatch("python", "install", "--private", dist_name)

    assert result.exit_code == 0, result.output
    assert result.output == helpers.dedent(
        f"""
        Installing {dist_name}
        Installed {dist_name} @ {dist_dir}
        """
    )

    install.assert_called_once_with(dist_name)
    path_append.assert_not_called()


def test_specific_location(hatch, helpers, temp_dir_data, path_append, dist_name, mocker):
    install_dir = temp_dir_data / "foo" / "bar" / "baz"
    dist_dir = install_dir / dist_name
    python_path = dist_dir / get_distribution(dist_name).python_path
    install = mocker.patch(
        "hatch.python.core.PythonManager.install", return_value=mocker.MagicMock(path=dist_dir, python_path=python_path)
    )

    result = hatch("python", "install", "--private", "-d", str(install_dir), dist_name)

    assert result.exit_code == 0, result.output
    assert result.output == helpers.dedent(
        f"""
        Installing {dist_name}
        Installed {dist_name} @ {dist_dir}
        """
    )

    install.assert_called_once_with(dist_name)
    path_append.assert_not_called()


def test_all(hatch, temp_dir_data, path_append, default_shells, mocker, compatible_python_distributions):
    mocked_dists = []
    for name in compatible_python_distributions:
        dist_dir = temp_dir_data / "data" / "pythons" / name
        python_path = dist_dir / get_distribution(name).python_path
        mocked_dists.append(mocker.MagicMock(path=dist_dir, python_path=python_path))

    install = mocker.patch("hatch.python.core.PythonManager.install", side_effect=mocked_dists)

    result = hatch("python", "install", "all")

    assert result.exit_code == 0, result.output

    expected_lines = []
    for dist in mocked_dists:
        expected_lines.extend((f"Installing {dist.path.name}", f"Installed {dist.path.name} @ {dist.path}"))

    expected_lines.extend((
        "",
        "The following directories have been added to your PATH (pending a shell restart):",
        "",
    ))
    expected_lines.extend(str(dist.python_path.parent) for dist in mocked_dists)
    expected_lines.append("")

    assert result.output == "\n".join(expected_lines)

    assert install.call_args_list == [mocker.call(name) for name in compatible_python_distributions]
    assert path_append.call_args_list == [
        mocker.call(str(dist.python_path.parent), shells=default_shells) for dist in mocked_dists
    ]
