from __future__ import annotations

import shutil

from pathlib import Path
from subprocess import CalledProcessError
from typing import TYPE_CHECKING
from typing import Any

import pytest

from cleo.io.null_io import NullIO
from packaging.utils import canonicalize_name
from poetry.core.packages.dependency import Dependency
from poetry.core.packages.directory_dependency import DirectoryDependency
from poetry.core.packages.file_dependency import FileDependency
from poetry.core.packages.package import Package
from poetry.core.packages.project_package import ProjectPackage
from poetry.core.packages.url_dependency import URLDependency
from poetry.core.packages.vcs_dependency import VCSDependency

from poetry.factory import Factory
from poetry.inspection.info import PackageInfo
from poetry.packages import DependencyPackage
from poetry.puzzle.provider import IncompatibleConstraintsError
from poetry.puzzle.provider import Provider
from poetry.repositories.exceptions import PackageNotFoundError
from poetry.repositories.repository import Repository
from poetry.repositories.repository_pool import Priority
from poetry.repositories.repository_pool import RepositoryPool
from poetry.utils.env import EnvCommandError
from poetry.utils.env import MockEnv as BaseMockEnv
from tests.helpers import get_dependency


if TYPE_CHECKING:
    from pathlib import Path

    from pytest_mock import MockerFixture

    from tests.types import FixtureDirGetter


SOME_URL = "https://example.com/path.tar.gz"


class MockEnv(BaseMockEnv):
    def run(self, bin: str, *args: str, **kwargs: Any) -> str:
        raise EnvCommandError(CalledProcessError(1, "python", output=""))


@pytest.fixture
def root() -> ProjectPackage:
    return ProjectPackage("root", "1.2.3")


@pytest.fixture
def repository() -> Repository:
    return Repository("repo")


@pytest.fixture
def pool(repository: Repository) -> RepositoryPool:
    pool = RepositoryPool()
    pool.add_repository(repository)

    return pool


@pytest.fixture
def provider(root: ProjectPackage, pool: RepositoryPool) -> Provider:
    return Provider(root, pool, NullIO())


@pytest.mark.parametrize(
    "dependency, expected",
    [
        (Dependency("foo", "<2"), [Package("foo", "1")]),
        (Dependency("foo", "<2", extras=["bar"]), [Package("foo", "1")]),
        (Dependency("foo", ">=1"), [Package("foo", "2"), Package("foo", "1")]),
        (Dependency("foo", ">=1a"), [Package("foo", "2"), Package("foo", "1")]),
        (
            Dependency("foo", ">=1", allows_prereleases=True),
            [
                Package("foo", "3a"),
                Package("foo", "2"),
                Package("foo", "2a"),
                Package("foo", "1"),
            ],
        ),
    ],
)
def test_search_for(
    provider: Provider,
    repository: Repository,
    dependency: Dependency,
    expected: list[Package],
) -> None:
    foo1 = Package("foo", "1")
    foo2a = Package("foo", "2a")
    foo2 = Package("foo", "2")
    foo3a = Package("foo", "3a")
    repository.add_package(foo1)
    repository.add_package(foo2a)
    repository.add_package(foo2)
    repository.add_package(foo3a)

    assert provider.search_for(dependency) == expected


@pytest.mark.parametrize(
    "dependency, direct_origin_dependency, expected_before, expected_after",
    [
        (
            Dependency("foo", ">=1"),
            URLDependency("foo", SOME_URL),
            [Package("foo", "3")],
            [Package("foo", "2a", source_type="url", source_url=SOME_URL)],
        ),
        (
            Dependency("foo", ">=2"),
            URLDependency("foo", SOME_URL),
            [Package("foo", "3")],
            [Package("foo", "3")],
        ),
        (
            Dependency("foo", ">=1", extras=["bar"]),
            URLDependency("foo", SOME_URL),
            [Package("foo", "3")],
            [Package("foo", "2a", source_type="url", source_url=SOME_URL)],
        ),
        (
            Dependency("foo", ">=1"),
            URLDependency("foo", SOME_URL, extras=["baz"]),
            [Package("foo", "3")],
            [Package("foo", "2a", source_type="url", source_url=SOME_URL)],
        ),
        (
            Dependency("foo", ">=1", extras=["bar"]),
            URLDependency("foo", SOME_URL, extras=["baz"]),
            [Package("foo", "3")],
            [Package("foo", "2a", source_type="url", source_url=SOME_URL)],
        ),
    ],
)
def test_search_for_direct_origin_and_extras(
    provider: Provider,
    repository: Repository,
    mocker: MockerFixture,
    dependency: Dependency,
    direct_origin_dependency: Dependency,
    expected_before: list[Package],
    expected_after: list[Package],
) -> None:
    foo2a_direct_origin = Package("foo", "2a", source_type="url", source_url=SOME_URL)
    mocker.patch(
        "poetry.puzzle.provider.Provider.search_for_direct_origin_dependency",
        return_value=foo2a_direct_origin,
    )
    foo2a = Package("foo", "2a")
    foo3 = Package("foo", "3")
    repository.add_package(foo2a)
    repository.add_package(foo3)

    assert provider.search_for(dependency) == expected_before
    assert provider.search_for(direct_origin_dependency) == [foo2a_direct_origin]
    assert provider.search_for(dependency) == expected_after


@pytest.mark.parametrize("value", [True, False])
def test_search_for_vcs_retains_develop_flag(provider: Provider, value: bool) -> None:
    dependency = VCSDependency(
        "demo", "git", "https://github.com/demo/demo.git", develop=value
    )
    package = provider.search_for_direct_origin_dependency(dependency)
    assert package.develop == value


def test_search_for_vcs_setup_egg_info(provider: Provider) -> None:
    dependency = VCSDependency("demo", "git", "https://github.com/demo/demo.git")

    package = provider.search_for_direct_origin_dependency(dependency)

    assert package.name == "demo"
    assert package.version.text == "0.1.2"

    required = {r for r in package.requires if not r.is_optional()}
    optional = {r for r in package.requires if r.is_optional()}
    assert required == {get_dependency("pendulum", ">=1.4.4")}
    assert optional == {get_dependency("tomlkit"), get_dependency("cleo")}
    assert package.extras == {
        "foo": [get_dependency("cleo")],
        "bar": [get_dependency("tomlkit")],
    }


def test_search_for_vcs_setup_egg_info_with_extras(provider: Provider) -> None:
    dependency = VCSDependency(
        "demo", "git", "https://github.com/demo/demo.git", extras=["foo"]
    )

    package = provider.search_for_direct_origin_dependency(dependency)

    assert package.name == "demo"
    assert package.version.text == "0.1.2"

    required = {r for r in package.requires if not r.is_optional()}
    optional = {r for r in package.requires if r.is_optional()}
    assert required == {get_dependency("pendulum", ">=1.4.4")}
    assert optional == {get_dependency("tomlkit"), get_dependency("cleo")}
    assert package.extras == {
        "foo": [get_dependency("cleo")],
        "bar": [get_dependency("tomlkit")],
    }


def test_search_for_vcs_read_setup(provider: Provider, mocker: MockerFixture) -> None:
    mocker.patch("poetry.utils.env.EnvManager.get", return_value=MockEnv())

    dependency = VCSDependency("demo", "git", "https://github.com/demo/demo.git")

    package = provider.search_for_direct_origin_dependency(dependency)

    assert package.name == "demo"
    assert package.version.text == "0.1.2"

    required = {r for r in package.requires if not r.is_optional()}
    optional = {r for r in package.requires if r.is_optional()}
    assert required == {get_dependency("pendulum", ">=1.4.4")}
    assert optional == {get_dependency("tomlkit"), get_dependency("cleo")}
    assert package.extras == {
        "foo": [get_dependency("cleo")],
        "bar": [get_dependency("tomlkit")],
    }


def test_search_for_vcs_read_setup_with_extras(
    provider: Provider, mocker: MockerFixture
) -> None:
    mocker.patch("poetry.utils.env.EnvManager.get", return_value=MockEnv())

    dependency = VCSDependency(
        "demo", "git", "https://github.com/demo/demo.git", extras=["foo"]
    )

    package = provider.search_for_direct_origin_dependency(dependency)

    assert package.name == "demo"
    assert package.version.text == "0.1.2"

    required = {r for r in package.requires if not r.is_optional()}
    optional = {r for r in package.requires if r.is_optional()}
    assert required == {get_dependency("pendulum", ">=1.4.4")}
    assert optional == {get_dependency("tomlkit"), get_dependency("cleo")}


def test_search_for_vcs_read_setup_raises_error_if_no_version(
    provider: Provider, mocker: MockerFixture
) -> None:
    mocker.patch(
        "poetry.inspection.info.get_pep517_metadata",
        return_value=PackageInfo(name="demo", version=None),
    )

    dependency = VCSDependency("demo", "git", "https://github.com/demo/no-version.git")

    with pytest.raises(RuntimeError):
        provider.search_for_direct_origin_dependency(dependency)


@pytest.mark.parametrize("directory", ["demo", "non-canonical-name"])
def test_search_for_directory_setup_egg_info(
    provider: Provider, directory: str, fixture_dir: FixtureDirGetter, tmp_path: Path
) -> None:
    path = shutil.copytree(
        fixture_dir("git") / "github.com" / "demo" / directory, tmp_path / "project"
    )
    dependency = DirectoryDependency("demo", path)

    package = provider.search_for_direct_origin_dependency(dependency)

    assert package.name == "demo"
    assert package.version.text == "0.1.2"

    required = {r for r in package.requires if not r.is_optional()}
    optional = {r for r in package.requires if r.is_optional()}
    assert required == {get_dependency("pendulum", ">=1.4.4")}
    assert optional == {get_dependency("tomlkit"), get_dependency("cleo")}
    assert package.extras == {
        "foo": [get_dependency("cleo")],
        "bar": [get_dependency("tomlkit")],
    }


def test_search_for_directory_setup_egg_info_with_extras(
    provider: Provider, fixture_dir: FixtureDirGetter, tmp_path: Path
) -> None:
    path = shutil.copytree(
        fixture_dir("git") / "github.com" / "demo" / "demo", tmp_path / "project"
    )

    dependency = DirectoryDependency("demo", path, extras=["foo"])

    package = provider.search_for_direct_origin_dependency(dependency)

    assert package.name == "demo"
    assert package.version.text == "0.1.2"

    required = {r for r in package.requires if not r.is_optional()}
    optional = {r for r in package.requires if r.is_optional()}
    assert required == {get_dependency("pendulum", ">=1.4.4")}
    assert optional == {get_dependency("tomlkit"), get_dependency("cleo")}
    assert package.extras == {
        "foo": [get_dependency("cleo")],
        "bar": [get_dependency("tomlkit")],
    }


@pytest.mark.parametrize("directory", ["demo", "non-canonical-name"])
def test_search_for_directory_setup_with_base(
    provider: Provider, directory: str, fixture_dir: FixtureDirGetter, tmp_path: Path
) -> None:
    path = shutil.copytree(
        fixture_dir("git") / "github.com" / "demo" / directory, tmp_path / "project"
    )

    dependency = DirectoryDependency("demo", path, base=path)

    package = provider.search_for_direct_origin_dependency(dependency)

    assert package.name == "demo"
    assert package.version.text == "0.1.2"

    required = {r for r in package.requires if not r.is_optional()}
    optional = {r for r in package.requires if r.is_optional()}
    assert required == {get_dependency("pendulum", ">=1.4.4")}
    assert optional == {get_dependency("tomlkit"), get_dependency("cleo")}
    assert package.extras == {
        "foo": [get_dependency("cleo")],
        "bar": [get_dependency("tomlkit")],
    }
    assert package.root_dir == path


def test_search_for_directory_setup_read_setup(
    provider: Provider,
    mocker: MockerFixture,
    fixture_dir: FixtureDirGetter,
    tmp_path: Path,
) -> None:
    mocker.patch("poetry.utils.env.EnvManager.get", return_value=MockEnv())

    path = shutil.copytree(
        fixture_dir("git") / "github.com" / "demo" / "demo", tmp_path / "project"
    )
    dependency = DirectoryDependency("demo", path)

    package = provider.search_for_direct_origin_dependency(dependency)

    assert package.name == "demo"
    assert package.version.text == "0.1.2"

    required = {r for r in package.requires if not r.is_optional()}
    optional = {r for r in package.requires if r.is_optional()}
    assert required == {get_dependency("pendulum", ">=1.4.4")}
    assert optional == {get_dependency("tomlkit"), get_dependency("cleo")}
    assert package.extras == {
        "foo": [get_dependency("cleo")],
        "bar": [get_dependency("tomlkit")],
    }


def test_search_for_directory_setup_read_setup_with_extras(
    provider: Provider,
    mocker: MockerFixture,
    fixture_dir: FixtureDirGetter,
    tmp_path: Path,
) -> None:
    mocker.patch("poetry.utils.env.EnvManager.get", return_value=MockEnv())

    path = shutil.copytree(
        fixture_dir("git") / "github.com" / "demo" / "demo", tmp_path / "project"
    )

    dependency = DirectoryDependency("demo", path, extras=["foo"])

    package = provider.search_for_direct_origin_dependency(dependency)

    assert package.name == "demo"
    assert package.version.text == "0.1.2"

    required = {r for r in package.requires if not r.is_optional()}
    optional = {r for r in package.requires if r.is_optional()}
    assert required == {get_dependency("pendulum", ">=1.4.4")}
    assert optional == {get_dependency("tomlkit"), get_dependency("cleo")}
    assert package.extras == {
        "foo": [get_dependency("cleo")],
        "bar": [get_dependency("tomlkit")],
    }


def test_search_for_directory_setup_read_setup_with_no_dependencies(
    provider: Provider, fixture_dir: FixtureDirGetter, tmp_path: Path
) -> None:
    path = shutil.copytree(
        fixture_dir("git") / "github.com" / "demo" / "no-dependencies",
        tmp_path / "project",
    )

    dependency = DirectoryDependency("demo", path)

    package = provider.search_for_direct_origin_dependency(dependency)

    assert package.name == "demo"
    assert package.version.text == "0.1.2"
    assert package.requires == []
    assert package.extras == {}


def test_search_for_directory_poetry(
    provider: Provider, fixture_dir: FixtureDirGetter, tmp_path: Path
) -> None:
    path = shutil.copytree(fixture_dir("project_with_extras"), tmp_path / "project")

    dependency = DirectoryDependency("project-with-extras", path)

    package = provider.search_for_direct_origin_dependency(dependency)

    assert package.name == "project-with-extras"
    assert package.version.text == "1.2.3"

    required = {
        r for r in sorted(package.requires, key=lambda r: r.name) if not r.is_optional()
    }
    optional = {
        r for r in sorted(package.requires, key=lambda r: r.name) if r.is_optional()
    }
    assert not required
    assert optional == {
        get_dependency("cachy", ">=0.2.0"),
        get_dependency("pendulum", ">=1.4.4"),
    }
    extras_a = canonicalize_name("extras-a")
    extras_b = canonicalize_name("extras-b")
    assert set(package.extras) == {extras_a, extras_b}
    assert set(package.extras[extras_a]) == {get_dependency("pendulum", ">=1.4.4")}
    assert set(package.extras[extras_b]) == {get_dependency("cachy", ">=0.2.0")}


def test_search_for_directory_poetry_with_extras(
    provider: Provider, fixture_dir: FixtureDirGetter, tmp_path: Path
) -> None:
    path = shutil.copytree(fixture_dir("project_with_extras"), tmp_path / "project")

    dependency = DirectoryDependency("project-with-extras", path, extras=["extras_a"])

    package = provider.search_for_direct_origin_dependency(dependency)

    assert package.name == "project-with-extras"
    assert package.version.text == "1.2.3"

    required = {
        r for r in sorted(package.requires, key=lambda r: r.name) if not r.is_optional()
    }
    optional = {
        r for r in sorted(package.requires, key=lambda r: r.name) if r.is_optional()
    }
    assert not required
    assert optional == {
        get_dependency("cachy", ">=0.2.0"),
        get_dependency("pendulum", ">=1.4.4"),
    }
    extras_a = canonicalize_name("extras-a")
    extras_b = canonicalize_name("extras-b")
    assert set(package.extras) == {extras_a, extras_b}
    assert set(package.extras[extras_a]) == {get_dependency("pendulum", ">=1.4.4")}
    assert set(package.extras[extras_b]) == {get_dependency("cachy", ">=0.2.0")}


def test_search_for_file_sdist(
    provider: Provider, fixture_dir: FixtureDirGetter
) -> None:
    dependency = FileDependency(
        "demo",
        fixture_dir("distributions") / "demo-0.1.0.tar.gz",
    )

    package = provider.search_for_direct_origin_dependency(dependency)

    assert package.name == "demo"
    assert package.version.text == "0.1.0"

    required = {
        r for r in sorted(package.requires, key=lambda r: r.name) if not r.is_optional()
    }
    optional = {
        r for r in sorted(package.requires, key=lambda r: r.name) if r.is_optional()
    }
    assert required == {get_dependency("pendulum", ">=1.4.4")}
    assert optional == {
        get_dependency("cleo"),
        get_dependency("tomlkit"),
    }
    assert package.extras == {
        "foo": [get_dependency("cleo")],
        "bar": [get_dependency("tomlkit")],
    }


def test_search_for_file_sdist_with_extras(
    provider: Provider, fixture_dir: FixtureDirGetter
) -> None:
    dependency = FileDependency(
        "demo",
        fixture_dir("distributions") / "demo-0.1.0.tar.gz",
        extras=["foo"],
    )

    package = provider.search_for_direct_origin_dependency(dependency)

    assert package.name == "demo"
    assert package.version.text == "0.1.0"

    required = {
        r for r in sorted(package.requires, key=lambda r: r.name) if not r.is_optional()
    }
    optional = {
        r for r in sorted(package.requires, key=lambda r: r.name) if r.is_optional()
    }
    assert required == {get_dependency("pendulum", ">=1.4.4")}
    assert optional == {
        get_dependency("cleo"),
        get_dependency("tomlkit"),
    }
    assert package.extras == {
        "foo": [get_dependency("cleo")],
        "bar": [get_dependency("tomlkit")],
    }


def test_search_for_file_wheel(
    provider: Provider, fixture_dir: FixtureDirGetter
) -> None:
    dependency = FileDependency(
        "demo",
        fixture_dir("distributions") / "demo-0.1.0-py2.py3-none-any.whl",
    )

    package = provider.search_for_direct_origin_dependency(dependency)

    assert package.name == "demo"
    assert package.version.text == "0.1.0"

    required = {
        r for r in sorted(package.requires, key=lambda r: r.name) if not r.is_optional()
    }
    optional = {
        r for r in sorted(package.requires, key=lambda r: r.name) if r.is_optional()
    }
    assert required == {get_dependency("pendulum", ">=1.4.4")}
    assert optional == {
        get_dependency("cleo"),
        get_dependency("tomlkit"),
    }
    assert package.extras == {
        "foo": [get_dependency("cleo")],
        "bar": [get_dependency("tomlkit")],
    }


def test_search_for_file_wheel_with_extras(
    provider: Provider, fixture_dir: FixtureDirGetter
) -> None:
    dependency = FileDependency(
        "demo",
        fixture_dir("distributions") / "demo-0.1.0-py2.py3-none-any.whl",
        extras=["foo"],
    )

    package = provider.search_for_direct_origin_dependency(dependency)

    assert package.name == "demo"
    assert package.version.text == "0.1.0"

    required = {
        r for r in sorted(package.requires, key=lambda r: r.name) if not r.is_optional()
    }
    optional = {
        r for r in sorted(package.requires, key=lambda r: r.name) if r.is_optional()
    }
    assert required == {get_dependency("pendulum", ">=1.4.4")}
    assert optional == {
        get_dependency("cleo"),
        get_dependency("tomlkit"),
    }
    assert package.extras == {
        "foo": [get_dependency("cleo")],
        "bar": [get_dependency("tomlkit")],
    }


def test_complete_package_merges_same_source_and_no_source(
    provider: Provider, root: ProjectPackage
) -> None:
    foo_no_source_1 = get_dependency("foo", ">=1")
    foo_source_1 = get_dependency("foo", "!=1.1.*")
    foo_source_1.source_name = "source"
    foo_source_2 = get_dependency("foo", "!=1.2.*")
    foo_source_2.source_name = "source"
    foo_no_source_2 = get_dependency("foo", "<2")

    root.add_dependency(foo_no_source_1)
    root.add_dependency(foo_source_1)
    root.add_dependency(foo_source_2)
    root.add_dependency(foo_no_source_2)

    complete_package = provider.complete_package(
        DependencyPackage(root.to_dependency(), root)
    )

    requires = complete_package.package.all_requires
    assert len(requires) == 1
    assert requires[0].source_name == "source"
    assert str(requires[0].constraint) in {
        ">=1,<1.1 || >=1.3,<2",
        ">=1,<1.1.dev0 || >=1.3.dev0,<2",
        ">=1,<1.1.0 || >=1.3.0,<2",
        ">=1,<1.1.0.dev0 || >=1.3.0.dev0,<2",
    }


def test_complete_package_does_not_merge_different_source_names(
    provider: Provider, root: ProjectPackage
) -> None:
    foo_source_1 = get_dependency("foo")
    foo_source_1.source_name = "source_1"
    foo_source_2 = get_dependency("foo")
    foo_source_2.source_name = "source_2"

    root.add_dependency(foo_source_1)
    root.add_dependency(foo_source_2)

    with pytest.raises(IncompatibleConstraintsError) as e:
        provider.complete_package(DependencyPackage(root.to_dependency(), root))

    expected = """\
Incompatible constraints in requirements of root (1.2.3):
foo ; source=source_2
foo ; source=source_1"""

    assert str(e.value) == expected


def test_complete_package_merges_same_source_type_and_no_source(
    provider: Provider, root: ProjectPackage, fixture_dir: FixtureDirGetter
) -> None:
    project_dir = fixture_dir("with_conditional_path_deps")
    path = (project_dir / "demo_one").as_posix()

    root.add_dependency(Factory.create_dependency("demo", ">=1.0"))
    root.add_dependency(Factory.create_dependency("demo", {"path": path}))
    root.add_dependency(Factory.create_dependency("demo", {"path": path}))  # duplicate
    root.add_dependency(Factory.create_dependency("demo", "<2.0"))

    complete_package = provider.complete_package(
        DependencyPackage(root.to_dependency(), root)
    )

    requires = complete_package.package.all_requires
    assert len(requires) == 1
    assert requires[0].source_url == path
    assert str(requires[0].constraint) == "1.2.3"


def test_complete_package_does_not_merge_different_source_types(
    provider: Provider, root: ProjectPackage, fixture_dir: FixtureDirGetter
) -> None:
    project_dir = fixture_dir("with_conditional_path_deps")
    for folder in ["demo_one", "demo_two"]:
        path = (project_dir / folder).as_posix()
        root.add_dependency(Factory.create_dependency("demo", {"path": path}))

    with pytest.raises(IncompatibleConstraintsError) as e:
        provider.complete_package(DependencyPackage(root.to_dependency(), root))

    expected = f"""\
Incompatible constraints in requirements of root (1.2.3):
demo @ {project_dir.as_uri()}/demo_two (1.2.3)
demo @ {project_dir.as_uri()}/demo_one (1.2.3)"""

    assert str(e.value) == expected


def test_complete_package_does_not_merge_different_source_type_and_name(
    provider: Provider, root: ProjectPackage, fixture_dir: FixtureDirGetter
) -> None:
    project_dir = fixture_dir("with_conditional_path_deps")
    path = (project_dir / "demo_one").as_posix()

    dep_with_source_name = Factory.create_dependency("demo", ">=1.0")
    dep_with_source_name.source_name = "source"
    root.add_dependency(dep_with_source_name)
    root.add_dependency(Factory.create_dependency("demo", {"path": path}))

    with pytest.raises(IncompatibleConstraintsError) as e:
        provider.complete_package(DependencyPackage(root.to_dependency(), root))

    expected = f"""\
Incompatible constraints in requirements of root (1.2.3):
demo @ {project_dir.as_uri()}/demo_one (1.2.3)
demo (>=1.0) ; source=source"""

    assert str(e.value) == expected


def test_complete_package_does_not_merge_different_subdirectories(
    provider: Provider, root: ProjectPackage
) -> None:
    dependency_one = Factory.create_dependency(
        "one",
        {
            "git": "https://github.com/demo/subdirectories.git",
            "subdirectory": "one",
        },
    )
    dependency_one_copy = Factory.create_dependency(
        "one",
        {
            "git": "https://github.com/demo/subdirectories.git",
            "subdirectory": "one-copy",
        },
    )

    root.add_dependency(dependency_one)
    root.add_dependency(dependency_one_copy)

    with pytest.raises(IncompatibleConstraintsError) as e:
        provider.complete_package(DependencyPackage(root.to_dependency(), root))

    expected = """\
Incompatible constraints in requirements of root (1.2.3):
one @ git+https://github.com/demo/subdirectories.git#subdirectory=one-copy (1.0.0)
one @ git+https://github.com/demo/subdirectories.git#subdirectory=one (1.0.0)"""

    assert str(e.value) == expected


@pytest.mark.parametrize("source_name", [None, "repo"])
def test_complete_package_with_extras_preserves_source_name(
    provider: Provider, repository: Repository, source_name: str | None
) -> None:
    package_a = Package("A", "1.0")
    package_b = Package("B", "1.0")
    dep = get_dependency("B", "^1.0", optional=True)
    package_a.add_dependency(dep)
    package_a.extras = {canonicalize_name("foo"): [dep]}
    repository.add_package(package_a)
    repository.add_package(package_b)

    dependency = Dependency("A", "1.0", extras=["foo"])
    if source_name:
        dependency.source_name = source_name

    complete_package = provider.complete_package(
        DependencyPackage(dependency, package_a)
    )

    requires = complete_package.package.all_requires
    assert len(requires) == 2
    assert requires[0].name == "a"
    assert requires[0].source_name == source_name
    assert requires[1].name == "b"
    assert requires[1].source_name is None


@pytest.mark.parametrize("with_extra", [False, True])
def test_complete_package_fetches_optional_vcs_dependency_only_if_requested(
    provider: Provider, repository: Repository, mocker: MockerFixture, with_extra: bool
) -> None:
    optional_vcs_dependency = Factory.create_dependency(
        "demo", {"git": "https://github.com/demo/demo.git", "optional": True}
    )
    package = Package("A", "1.0", features=["foo"] if with_extra else [])
    package.add_dependency(optional_vcs_dependency)
    package.extras = {canonicalize_name("foo"): [optional_vcs_dependency]}
    repository.add_package(package)

    spy = mocker.spy(provider, "_search_for_vcs")

    provider.complete_package(DependencyPackage(package.to_dependency(), package))

    if with_extra:
        spy.assert_called()
    else:
        spy.assert_not_called()


def test_complete_package_finds_locked_package_in_explicit_source(
    root: ProjectPackage, pool: RepositoryPool
) -> None:
    package = Package("a", "1.0", source_reference="explicit")
    explicit_repo = Repository("explicit")
    explicit_repo.add_package(package)
    pool.add_repository(explicit_repo, priority=Priority.EXPLICIT)

    root_dependency = get_dependency("a", ">0")
    root_dependency.source_name = "explicit"
    root.add_dependency(root_dependency)
    locked_package = Package("a", "1.0", source_reference="explicit")
    provider = Provider(root, pool, NullIO(), locked=[locked_package])
    provider.complete_package(DependencyPackage(root.to_dependency(), root))

    # transitive dependency without explicit source
    dependency = get_dependency("a", ">=1")

    locked = provider.get_locked(dependency)
    assert locked is not None
    provider.complete_package(locked)  # must not fail


def test_complete_package_finds_locked_package_in_other_source(
    root: ProjectPackage, repository: Repository, pool: RepositoryPool
) -> None:
    package = Package("a", "1.0")
    repository.add_package(package)
    explicit_repo = Repository("explicit")
    pool.add_repository(explicit_repo)

    root_dependency = get_dependency("a", ">0")  # no explicit source
    root.add_dependency(root_dependency)
    locked_package = Package("a", "1.0", source_reference="explicit")  # explicit source
    provider = Provider(root, pool, NullIO(), locked=[locked_package])
    provider.complete_package(DependencyPackage(root.to_dependency(), root))

    # transitive dependency without explicit source
    dependency = get_dependency("a", ">=1")

    locked = provider.get_locked(dependency)
    assert locked is not None
    provider.complete_package(locked)  # must not fail


def test_complete_package_raises_packagenotfound_if_locked_source_not_available(
    root: ProjectPackage, pool: RepositoryPool, provider: Provider
) -> None:
    locked_package = Package("a", "1.0", source_reference="outdated")
    provider = Provider(root, pool, NullIO(), locked=[locked_package])
    provider.complete_package(DependencyPackage(root.to_dependency(), root))

    # transitive dependency without explicit source
    dependency = get_dependency("a", ">=1")

    locked = provider.get_locked(dependency)
    assert locked is not None
    with pytest.raises(PackageNotFoundError):
        provider.complete_package(locked)


def test_source_dependency_is_satisfied_by_direct_origin(
    provider: Provider, repository: Repository
) -> None:
    direct_origin_package = Package("foo", "1.1", source_type="url")
    repository.add_package(Package("foo", "1.0"))
    provider._direct_origin_packages = {"foo": direct_origin_package}
    dep = Dependency("foo", ">=1")

    assert provider.search_for(dep) == [direct_origin_package]


def test_explicit_source_dependency_is_not_satisfied_by_direct_origin(
    provider: Provider, repository: Repository
) -> None:
    repo_package = Package("foo", "1.0")
    repository.add_package(repo_package)
    provider._direct_origin_packages = {"foo": Package("foo", "1.1", source_type="url")}
    dep = Dependency("foo", ">=1")
    dep.source_name = repository.name

    assert provider.search_for(dep) == [repo_package]


def test_source_dependency_is_not_satisfied_by_incompatible_direct_origin(
    provider: Provider, repository: Repository
) -> None:
    repo_package = Package("foo", "2.0")
    repository.add_package(repo_package)
    provider._direct_origin_packages = {"foo": Package("foo", "1.0", source_type="url")}
    dep = Dependency("foo", ">=2")
    dep.source_name = repository.name

    assert provider.search_for(dep) == [repo_package]
