import shutil

import pytest
from unearth import Link

from pdm.cli.commands.add import Command
from pdm.cli.filters import GroupSelection
from pdm.models.markers import EnvSpec
from pdm.models.specifiers import PySpecSet
from pdm.pytest import Distribution
from pdm.utils import cd
from tests import FIXTURES


def make_workspace_member(project, core):
    project.pyproject.settings["workspace"] = {"members": ["packages/*"]}
    project.pyproject.write()
    member_path = project.root / "packages" / "foo"
    member_path.mkdir(parents=True)
    member_path.joinpath("pyproject.toml").write_text(
        '[project]\nname = "foo"\nversion = "0.1.0"\ndependencies = []\n',
        encoding="utf-8",
    )
    return core.create_project(member_path, global_config=project.global_config.config_file.as_posix())


def test_add_package(project, working_set, dev_option, pdm):
    pdm(["add", *dev_option, "requests"], obj=project, strict=True)
    group = project.pyproject.dependency_groups["dev"] if dev_option else project.pyproject.metadata["dependencies"]

    assert group[0] == "requests>=2.19.1"
    locked_candidates = project.get_locked_repository().candidates
    assert locked_candidates["idna"].version == "2.7"
    for package in ("requests", "idna", "chardet", "urllib3", "certifi"):
        assert package in working_set


def test_add_package_no_lock(project, working_set, dev_option, pdm):
    pdm(["add", *dev_option, "--frozen-lockfile", "-v", "requests"], obj=project, strict=True)
    group = project.pyproject.dependency_groups["dev"] if dev_option else project.pyproject.metadata["dependencies"]

    assert group[0] == "requests>=2.19.1"
    assert not project.lockfile.exists()
    for package in ("requests", "idna", "chardet", "urllib3", "certifi"):
        assert package in working_set


def test_add_command(project, pdm, mocker):
    do_add = mocker.patch("pdm.cli.commands.add.Command.do_add")
    pdm(["add", "requests"], obj=project)
    do_add.assert_called_once()


def test_add_workspace_member_updates_root_lockfile(project, core, repository):
    member_project = make_workspace_member(project, core)

    Command.do_add(
        member_project,
        selection=GroupSelection(member_project),
        packages=["requests"],
        sync=False,
    )

    assert (project.root / "pdm.lock").exists()
    assert not (member_project.root / "pdm.lock").exists()
    locked_candidates = member_project.get_locked_repository().candidates
    assert "foo" in locked_candidates
    assert "requests" in locked_candidates


def test_add_subdirectory_path_updates_workspace(project, repository):
    member_path = project.root / "packages" / "foo"
    member_path.mkdir(parents=True)
    member_path.joinpath("pyproject.toml").write_text(
        '[project]\nname = "foo"\nversion = "0.1.0"\ndependencies = []\n',
        encoding="utf-8",
    )

    with cd(project.root):
        Command.do_add(
            project,
            selection=GroupSelection(project),
            packages=["packages/foo"],
            sync=False,
        )

    project.pyproject.reload()
    assert project.pyproject.settings["workspace"]["members"] == ["packages/foo"]
    assert project.pyproject.metadata["dependencies"] == ["foo @ file:///${PROJECT_ROOT}/packages/foo"]
    locked_candidates = project.get_locked_repository().candidates
    assert locked_candidates["foo"].req.editable
    assert locked_candidates["foo"].req.str_path == "./packages/foo"


def test_add_package_to_custom_group(project, working_set, pdm):
    pdm(["add", "requests", "--group", "test"], obj=project, strict=True)

    assert "requests" in project.pyproject.metadata["optional-dependencies"]["test"][0]
    locked_candidates = project.get_locked_repository().candidates
    assert locked_candidates["idna"].version == "2.7"
    for package in ("requests", "idna", "chardet", "urllib3", "certifi"):
        assert package in working_set


def test_add_package_to_custom_dev_group(project, working_set, pdm):
    pdm(["add", "requests", "--group", "test", "--dev"], obj=project, strict=True)

    dependencies = project.pyproject.dependency_groups["test"]
    assert "requests" in dependencies[0]
    locked_candidates = project.get_locked_repository().candidates
    assert locked_candidates["idna"].version == "2.7"
    for package in ("requests", "idna", "chardet", "urllib3", "certifi"):
        assert package in working_set


@pytest.mark.usefixtures("vcs")
def test_add_editable_package(project, working_set, pdm):
    # Ensure that correct python version is used.
    project.environment.python_requires = PySpecSet(">=3.6")
    pdm(["add", "--dev", "demo"], obj=project, strict=True)
    pdm(["add", "-de", "git+https://github.com/test-root/demo.git#egg=demo"], obj=project, strict=True)

    group = project.pyproject.dev_dependencies["dev"]
    assert group == ["-e git+https://github.com/test-root/demo.git#egg=demo"]
    assert not project.pyproject.dependency_groups
    locked_candidates = project.get_locked_repository().candidates
    assert locked_candidates["demo"].prepare(project.environment).revision == "1234567890abcdef"
    assert working_set["demo"].link_file
    assert locked_candidates["idna"].version == "2.7"
    assert "idna" in working_set

    pdm(["sync", "--no-editable"], obj=project, strict=True)
    assert not working_set["demo"].link_file


@pytest.mark.usefixtures("vcs", "working_set")
def test_add_editable_package_to_metadata_forbidden(project, pdm):
    project.environment.python_requires = PySpecSet(">=3.6")
    result = pdm(["add", "-v", "-e", "git+https://github.com/test-root/demo.git#egg=demo"], obj=project)
    assert "PdmUsageError" in result.stderr
    result = pdm(["add", "-v", "-Gtest", "-e", "git+https://github.com/test-root/demo.git#egg=demo"], obj=project)
    assert "PdmUsageError" in result.stderr


@pytest.mark.usefixtures("working_set", "vcs")
def test_non_editable_override_editable(project, pdm):
    project.environment.python_requires = PySpecSet(">=3.6")
    url = "git+https://github.com/test-root/demo.git#egg=demo"
    pdm(["add", "--dev", "-e", url], obj=project, strict=True)
    pdm(["add", "--dev", url], obj=project, strict=True)
    assert not project.get_dependencies("dev")[0].editable


@pytest.mark.usefixtures("working_set", "vcs")
def test_add_editable_normal_dev_dependency(project, pdm):
    project.environment.python_requires = PySpecSet(">=3.6")
    url = "git+https://github.com/test-root/demo.git#egg=demo"
    pdm(["add", "--dev", "-e", url], obj=project, strict=True)
    pdm(["add", "-d", "urllib3"], obj=project, strict=True)
    pdm(["add", "-d", "idna"], obj=project, strict=True)
    dev_group = project.pyproject.settings["dev-dependencies"]["dev"]
    pep735_group = project.pyproject.dependency_groups["dev"]
    assert dev_group == ["-e git+https://github.com/test-root/demo.git#egg=demo"]
    assert pep735_group == ["urllib3>=1.22", "idna>=2.7"]


@pytest.mark.usefixtures("working_set", "vcs")
def test_add_dev_dependency_with_existing_editables_group(project, pdm):
    project.environment.python_requires = PySpecSet(">=3.6")
    url = "git+https://github.com/test-root/demo.git#egg=demo"
    pdm(["add", "-dG", "editables", "-e", url], obj=project, strict=True)
    pdm(["add", "-d", "urllib3"], obj=project, strict=True)
    pdm(["add", "-dG", "named", "idna"], obj=project, strict=True)
    assert "editables" in project.pyproject.settings["dev-dependencies"]
    assert "dev" in project.pyproject.dependency_groups
    assert "named" in project.pyproject.dependency_groups
    editables_group = project.pyproject.settings["dev-dependencies"]["editables"]
    pep735_group = project.pyproject.dependency_groups["dev"]
    pep735_named_group = project.pyproject.dependency_groups["named"]
    assert editables_group == ["-e git+https://github.com/test-root/demo.git#egg=demo"]
    assert "editables" not in project.pyproject.dependency_groups
    assert pep735_group == ["urllib3>=1.22"]
    assert pep735_named_group == ["idna>=2.7"]


@pytest.mark.usefixtures("working_set")
def test_add_remote_package_url(project, dev_option, pdm):
    project.environment.python_requires = PySpecSet(">=3.6")
    url = "http://fixtures.test/artifacts/demo-0.0.1-py2.py3-none-any.whl"
    pdm(["add", *dev_option, url], obj=project, strict=True)
    group = project.pyproject.dependency_groups["dev"] if dev_option else project.pyproject.metadata["dependencies"]
    assert group[0] == f"demo @ {url}"


def test_add_no_install(project, working_set, pdm):
    pdm(["add", "--no-sync", "requests"], obj=project, strict=True)
    for package in ("requests", "idna", "chardet", "urllib3", "certifi"):
        assert package not in working_set


def test_add_no_sync_frozen_lockfile_skips_resolution(project, pdm, mocker):
    do_lock = mocker.patch("pdm.cli.actions.do_lock")
    pdm(["add", "--frozen-lockfile", "--no-sync", "requests"], obj=project, strict=True)

    do_lock.assert_not_called()
    assert project.pyproject.metadata["dependencies"][0] == "requests"
    assert not project.lockfile.exists()


@pytest.mark.usefixtures("repository")
def test_add_package_save_exact(project, pdm):
    pdm(["add", "--save-exact", "--no-sync", "requests"], obj=project, strict=True)
    assert project.pyproject.metadata["dependencies"][0] == "requests==2.19.1"


@pytest.mark.usefixtures("repository")
def test_add_package_save_wildcard(project, pdm):
    pdm(["add", "--save-wildcard", "--no-sync", "requests"], obj=project, strict=True)
    assert project.pyproject.metadata["dependencies"][0] == "requests"


@pytest.mark.usefixtures("repository")
def test_add_package_save_minimum(project, pdm):
    pdm(["add", "--save-minimum", "--no-sync", "requests"], obj=project, strict=True)
    assert project.pyproject.metadata["dependencies"][0] == "requests>=2.19.1"


def test_add_package_update_reuse(project, repository, pdm):
    pdm(["add", "--no-sync", "--save-wildcard", "requests", "pytz"], obj=project, strict=True)

    locked_candidates = project.get_locked_repository().candidates
    assert locked_candidates["requests"].version == "2.19.1"
    assert locked_candidates["chardet"].version == "3.0.4"
    assert locked_candidates["pytz"].version == "2019.3"

    repository.add_candidate("pytz", "2019.6")
    repository.add_candidate("chardet", "3.0.5")
    repository.add_candidate("requests", "2.20.0")
    repository.add_dependencies(
        "requests",
        "2.20.0",
        [
            "certifi>=2017.4.17",
            "chardet<3.1.0,>=3.0.2",
            "idna<2.8,>=2.5",
            "urllib3<1.24,>=1.21.1",
        ],
    )
    pdm(["add", "--no-sync", "--save-wildcard", "requests"], obj=project, strict=True)
    locked_candidates = project.get_locked_repository().candidates
    assert locked_candidates["requests"].version == "2.20.0"
    assert locked_candidates["chardet"].version == "3.0.4"
    assert locked_candidates["pytz"].version == "2019.3"


def test_add_package_update_eager(project, repository, pdm):
    pdm(["add", "--no-sync", "--save-wildcard", "requests", "pytz"], obj=project, strict=True)

    locked_candidates = project.get_locked_repository().candidates
    assert locked_candidates["requests"].version == "2.19.1"
    assert locked_candidates["chardet"].version == "3.0.4"
    assert locked_candidates["pytz"].version == "2019.3"

    repository.add_candidate("pytz", "2019.6")
    repository.add_candidate("chardet", "3.0.5")
    repository.add_candidate("requests", "2.20.0")
    repository.add_dependencies(
        "requests",
        "2.20.0",
        [
            "certifi>=2017.4.17",
            "chardet<3.1.0,>=3.0.2",
            "idna<2.8,>=2.5",
            "urllib3<1.24,>=1.21.1",
        ],
    )
    pdm(["add", "--no-sync", "--save-wildcard", "--update-eager", "requests"], obj=project, strict=True)
    locked_candidates = project.get_locked_repository().candidates
    assert locked_candidates["requests"].version == "2.20.0"
    assert locked_candidates["chardet"].version == "3.0.5"
    assert locked_candidates["pytz"].version == "2019.3"


def test_add_package_with_mismatch_marker(project, working_set, pdm):
    env = project.environment
    env.__dict__["spec"] = EnvSpec.from_spec("==3.11", "macos", "cpython")
    pdm(["add", "requests", "pytz; platform_system!='Darwin'"], obj=project, strict=True)
    assert "pytz" not in working_set


def test_add_dependency_from_multiple_parents(project, working_set, pdm):
    env = project.environment
    env.__dict__["spec"] = EnvSpec.from_spec("==3.11", "macos", "cpython")
    pdm(["add", "requests", "chardet; platform_system!='Darwin'"], obj=project, strict=True)
    assert "chardet" in working_set


def test_add_packages_without_self(project, working_set, pdm):
    project.environment.python_requires = PySpecSet(">=3.6")
    pdm(["add", "--no-self", "requests"], obj=project, strict=True)
    assert project.name not in working_set


@pytest.mark.usefixtures("working_set")
def test_add_package_unconstrained_rewrite_specifier(project, pdm):
    project.environment.python_requires = PySpecSet(">=3.6")
    pdm(["add", "--no-self", "django"], obj=project, strict=True)
    locked_candidates = project.get_locked_repository().candidates
    assert locked_candidates["django"].version == "2.2.9"
    assert project.pyproject.metadata["dependencies"][0] == "django>=2.2.9"

    pdm(["add", "--no-self", "--unconstrained", "django-toolbar"], obj=project, strict=True)
    locked_candidates = project.get_locked_repository().candidates
    assert locked_candidates["django"].version == "1.11.8"
    assert project.pyproject.metadata["dependencies"][0] == "django>=1.11.8"


@pytest.mark.usefixtures("working_set", "vcs")
def test_add_cached_vcs_requirement(project, mocker, pdm):
    project.environment.python_requires = PySpecSet(">=3.6")
    url = "git+https://github.com/test-root/demo.git@1234567890abcdef#egg=demo"
    built_path = FIXTURES / "artifacts/demo-0.0.1-py2.py3-none-any.whl"
    wheel_cache = project.make_wheel_cache()
    cache_path = wheel_cache.get_path_for_link(Link(url), project.environment.spec)
    if not cache_path.exists():
        cache_path.mkdir(parents=True)
    shutil.copy2(built_path, cache_path)
    downloader = mocker.patch("unearth.finder.unpack_link")
    builder = mocker.patch("pdm.builders.WheelBuilder.build")
    pdm(["add", "--no-self", url], obj=project, strict=True)
    lockfile_entry = next(p for p in project.lockfile["package"] if p["name"] == "demo")
    assert lockfile_entry["revision"] == "1234567890abcdef"
    downloader.assert_not_called()
    builder.assert_not_called()


@pytest.mark.usefixtures("repository")
def test_add_with_dry_run(project, pdm):
    result = pdm(["add", "--dry-run", "requests"], obj=project, strict=True)
    project.pyproject.reload()
    assert not project.get_dependencies()
    assert "requests 2.19.1" in result.stdout
    assert "urllib3 1.22" in result.stdout


def test_add_with_prerelease(project, working_set, pdm):
    pdm(["add", "--prerelease", "--save-compatible", "urllib3"], obj=project, strict=True)
    assert working_set["urllib3"].version == "1.23b0"
    assert project.pyproject.metadata["dependencies"][0] == "urllib3<2,>=1.23b0"


def test_add_editable_package_with_extras(project, working_set, pdm):
    project.environment.python_requires = PySpecSet(">=3.6")
    dep_path = FIXTURES.joinpath("projects/demo")
    pdm(["add", "-dGdev", "-e", f"{dep_path.as_posix()}[security]"], obj=project, strict=True)
    assert f"-e {dep_path.as_uri()}#egg=demo[security]" in project.use_pyproject_dependencies("dev", True)[0]
    assert "demo" in working_set
    assert "requests" in working_set
    assert "urllib3" in working_set


@pytest.mark.usefixtures("repository")
def test_add_dependency_with_extras(project, pdm):
    project.environment.python_requires = PySpecSet(">=3.6")
    pdm(["add", "requests[security]", "--no-sync"], obj=project, strict=True)
    locked_repo = project.get_locked_repository()
    extra_package = next(p for p in locked_repo.packages.values() if p.candidate.identify() == "requests[security]")
    assert extra_package.dependencies == ["pyOpenSSL>=0.14", "requests==2.19.1"]
    # test adding new package won't add duplicate dependencies
    pdm(["add", "pytz", "--no-sync"], obj=project, strict=True)
    locked_repo = project.get_locked_repository()
    extra_package = next(p for p in locked_repo.packages.values() if p.candidate.identify() == "requests[security]")
    assert extra_package.dependencies == ["pyOpenSSL>=0.14", "requests==2.19.1"]


def test_add_package_with_local_version(project, repository, working_set, pdm):
    repository.add_candidate("foo", "1.0a0+local")
    pdm(["add", "-v", "foo"], obj=project, strict=True)
    assert working_set["foo"].version == "1.0a0+local"
    dependencies, _ = project.use_pyproject_dependencies("default")
    assert dependencies[0] == "foo>=1.0a0"


def test_add_group_to_lockfile(project, working_set, pdm):
    pdm(["add", "requests"], obj=project, strict=True)
    assert project.lockfile.groups == ["default"]
    pdm(["add", "--group", "tz", "pytz"], obj=project, strict=True)
    assert project.lockfile.groups == ["default", "tz"]
    assert "pytz" in working_set


def test_add_group_to_lockfile_without_package(project, working_set, pdm):
    project.add_dependencies(["requests"])
    project.add_dependencies(["pytz"], to_group="tz")
    pdm(["install"], obj=project, strict=True)
    assert "pytz" not in working_set
    assert project.lockfile.groups == ["default"]
    pdm(["add", "--group", "tz"], obj=project, strict=True)
    assert project.lockfile.groups == ["default", "tz"]
    assert "pytz" in working_set


def test_add_update_reuse_installed(project, working_set, repository, pdm):
    working_set["foo"] = Distribution("foo", "1.0.0")
    repository.add_candidate("foo", "1.0.0")
    repository.add_candidate("foo", "1.1.0")
    pdm(["add", "--update-reuse-installed", "foo"], obj=project, strict=True)
    locked_candidates = project.get_locked_repository().candidates
    assert locked_candidates["foo"].version == "1.0.0"


def test_add_update_reuse_installed_config(project, working_set, repository, pdm):
    working_set["foo"] = Distribution("foo", "1.0.0")
    repository.add_candidate("foo", "1.0.0")
    repository.add_candidate("foo", "1.1.0")
    project.project_config["strategy.update"] = "reuse-installed"
    pdm(["add", "foo"], obj=project, strict=True)
    locked_candidates = project.get_locked_repository().candidates
    assert locked_candidates["foo"].version == "1.0.0"


def test_add_disable_cache(project, pdm, working_set):
    cache_dir = project.cache_dir
    pdm(["--no-cache", "add", "requests"], obj=project, strict=True)
    assert "requests" in working_set

    files = [file for file in cache_dir.rglob("*") if file.is_file()]
    assert not files


@pytest.mark.usefixtures("working_set")
def test_add_dependency_with_direct_minimal_versions(project, pdm, repository):
    pdm(["lock", "-S", "direct_minimal_versions"], obj=project, strict=True)
    repository.add_candidate("pytz", "2019.6")
    pdm(["add", "django"], obj=project, strict=True)
    all_candidates = project.get_locked_repository().candidates
    assert "django>=1.11.8" in project.pyproject.metadata["dependencies"]
    assert all_candidates["django"].version == "1.11.8"
    assert all_candidates["pytz"].version == "2019.6"


def test_add_group_with_normalized_name(project, pdm, working_set):
    project.pyproject.dependency_groups.update({"foo_bar": ["requests"]})
    project.pyproject.write()
    pdm(["lock"], obj=project, strict=True)
    assert "foo-bar" in project.lockfile.groups
    pdm(["sync", "-G", "foo.bar"], obj=project, strict=True)
    assert "requests" in working_set
    result = pdm(["add", "-G", "foo-bar", "pytz"], obj=project)
    assert result.exit_code != 0
    assert "Group foo-bar already exists in another non-normalized form" in result.stderr


@pytest.mark.usefixtures("working_set")
def test_add_to_dependency_group_with_include(project, pdm):
    from pdm.formats.base import make_array

    project.pyproject.dependency_groups.update({"tz": ["pytz"], "web": make_array([{"include-group": "tz"}])})
    project.pyproject.write()
    pdm(["add", "-Gweb", "requests"], obj=project, strict=True)
    assert project.pyproject.dependency_groups["web"] == [{"include-group": "tz"}, "requests>=2.19.1"]
