import io
import os
from distutils.version import StrictVersion
from typing import List, Optional, Tuple
from unittest import mock
from zipfile import ZipFile

import pytest
from pydantic import ValidationError

from cumulusci.core.config.OrgConfig import OrgConfig, VersionInfo
from cumulusci.core.config.project_config import BaseProjectConfig
from cumulusci.core.dependencies.dependencies import (
    DynamicDependency,
    GitHubDynamicDependency,
    GitHubDynamicSubfolderDependency,
    PackageNamespaceVersionDependency,
    PackageVersionIdDependency,
    StaticDependency,
    UnmanagedGitHubRefDependency,
    UnmanagedZipURLDependency,
    parse_dependency,
)
from cumulusci.core.dependencies.resolvers import DependencyResolutionStrategy, Resolver
from cumulusci.core.exceptions import DependencyResolutionError
from cumulusci.salesforce_api.package_install import (
    DEFAULT_PACKAGE_RETRY_OPTIONS,
    PackageInstallOptions,
)
from cumulusci.utils.ziputils import zip_subfolder


class ConcreteDynamicDependency(DynamicDependency):
    ref: Optional[str]
    resolved: Optional[bool] = False

    @property
    def is_resolved(self):
        return self.resolved

    def resolve(
        self, context: BaseProjectConfig, strategies: List[DependencyResolutionStrategy]
    ):
        super().resolve(context, strategies)
        self.resolved = True

    @property
    def name(self):
        return ""


class MockResolver(Resolver):
    def __init__(
        self,
        resolve_ref: Optional[str] = None,
        resolve_dep: Optional[StaticDependency] = None,
    ):
        self.ref = resolve_ref
        self.dep = resolve_dep

    def can_resolve(self, dep: DynamicDependency, context: BaseProjectConfig) -> bool:
        return bool(self.dep and self.ref)

    def resolve(
        self, dep: DynamicDependency, context: BaseProjectConfig
    ) -> Tuple[Optional[str], Optional[StaticDependency]]:
        return self.ref, self.dep


class MockBadResolver(Resolver):
    def can_resolve(self, dep: DynamicDependency, context: BaseProjectConfig) -> bool:
        return True

    def resolve(
        self, dep: DynamicDependency, context: BaseProjectConfig
    ) -> Tuple[Optional[str], Optional[StaticDependency]]:
        raise DependencyResolutionError("Bad resolver")


class TestDynamicDependency:
    @mock.patch("cumulusci.core.dependencies.resolvers.get_resolver")
    def test_dynamic_dependency(self, get_resolver):
        d = ConcreteDynamicDependency()
        resolvers = [
            MockResolver(),
            MockResolver(
                "aaaaaaaaaaaaaaaa",
                PackageNamespaceVersionDependency(namespace="foo", version="1.0"),
            ),
        ]
        get_resolver.side_effect = resolvers

        d.resolve(
            mock.Mock(),
            [
                DependencyResolutionStrategy.UNMANAGED_HEAD,
                DependencyResolutionStrategy.COMMIT_STATUS_PREVIOUS_RELEASE_BRANCH,
            ],
        )

        assert d.managed_dependency == PackageNamespaceVersionDependency(
            namespace="foo", version="1.0"
        )
        assert d.ref == "aaaaaaaaaaaaaaaa"

    @mock.patch("cumulusci.core.dependencies.resolvers.get_resolver")
    def test_dynamic_dependency__twice(self, get_resolver):
        d = ConcreteDynamicDependency()
        resolvers = [
            mock.Mock(
                wraps=MockResolver(
                    "aaaaaaaaaaaaaaaa",
                    PackageNamespaceVersionDependency(namespace="foo", version="1.0"),
                )
            ),
        ]
        get_resolver.side_effect = resolvers

        d.resolve(
            mock.Mock(),
            [
                DependencyResolutionStrategy.UNMANAGED_HEAD,
                DependencyResolutionStrategy.COMMIT_STATUS_PREVIOUS_RELEASE_BRANCH,
            ],
        )

        assert d.is_resolved
        resolvers[0].resolve.assert_called_once()
        d.resolve(
            mock.Mock(),
            [
                DependencyResolutionStrategy.UNMANAGED_HEAD,
                DependencyResolutionStrategy.COMMIT_STATUS_PREVIOUS_RELEASE_BRANCH,
            ],
        )
        assert d.is_resolved
        resolvers[0].resolve.assert_called_once()

    @mock.patch("cumulusci.core.dependencies.resolvers.get_resolver")
    def test_dynamic_dependency_resolution_fails(self, get_resolver):
        d = ConcreteDynamicDependency()
        resolvers = [MockBadResolver()]
        get_resolver.side_effect = resolvers

        with pytest.raises(DependencyResolutionError):
            d.resolve(mock.Mock(), [DependencyResolutionStrategy.UNMANAGED_HEAD])

    @mock.patch("cumulusci.core.dependencies.resolvers.get_resolver")
    def test_dynamic_dependency_resolution_no_results(self, get_resolver):
        d = ConcreteDynamicDependency()
        resolvers = [MockResolver("", None)]
        get_resolver.side_effect = resolvers

        with pytest.raises(DependencyResolutionError):
            d.resolve(mock.Mock(), [DependencyResolutionStrategy.UNMANAGED_HEAD])


class TestGitHubDynamicSubfolderDependency:
    def test_flatten(self):
        gh = GitHubDynamicSubfolderDependency(
            github="https://github.com/Test/TestRepo", subfolder="foo"
        )

        gh.ref = "aaaa"

        assert gh.is_unmanaged
        assert gh.flatten(mock.Mock()) == [
            UnmanagedGitHubRefDependency(
                github="https://github.com/Test/TestRepo",
                subfolder="foo",
                ref="aaaa",
                namespace_inject=None,
                namespace_strip=None,
            )
        ]

    def test_flatten__unresolved(self):
        context = mock.Mock()
        gh = GitHubDynamicSubfolderDependency(
            repo_owner="Test", repo_name="TestRepo", subfolder="foo"
        )

        with pytest.raises(DependencyResolutionError) as e:
            gh.flatten(context)

        assert "is not resolved" in str(e)

    def test_name(self):
        gh = GitHubDynamicSubfolderDependency(
            repo_owner="Test", repo_name="TestRepo", subfolder="foo"
        )
        assert gh.github in gh.name and gh.subfolder in gh.name

    def test_description(self):
        gh = GitHubDynamicSubfolderDependency(
            repo_owner="Test", repo_name="TestRepo", subfolder="foo"
        )
        gh.ref = "aaaa"
        assert (
            gh.github in gh.description
            and gh.subfolder in gh.description
            and gh.ref in gh.description
        )


class TestGitHubDynamicDependency:
    def test_create_repo_name(self):
        gh = GitHubDynamicDependency(repo_owner="Test", repo_name="TestRepo")
        assert gh.github == "https://github.com/Test/TestRepo"

    def test_create_failure(self):
        with pytest.raises(ValidationError):
            GitHubDynamicDependency(repo_owner="Test")

        with pytest.raises(ValidationError):
            GitHubDynamicDependency(
                github="http://github.com/Test/TestRepo", tag="tag/1.0", ref="aaaaa"
            )

        with pytest.raises(ValidationError):
            GitHubDynamicDependency(
                github="http://github.com/Test/TestRepo", namespace_inject="foo"
            )

    def test_flatten(self, project_config):
        gh = GitHubDynamicDependency(github="https://github.com/SFDO-Tooling/RootRepo")
        gh.ref = "aaaaa"
        gh.managed_dependency = PackageNamespaceVersionDependency(
            namespace="bar", version="2.0"
        )

        assert gh.flatten(project_config) == [
            GitHubDynamicDependency(
                github="https://github.com/SFDO-Tooling/DependencyRepo",
                password_env_name="DEP_PW",
            ),
            UnmanagedGitHubRefDependency(
                github="https://github.com/SFDO-Tooling/RootRepo",
                subfolder="unpackaged/pre/first",
                unmanaged=True,
                ref="aaaaa",
            ),
            UnmanagedGitHubRefDependency(
                github="https://github.com/SFDO-Tooling/RootRepo",
                subfolder="unpackaged/pre/second",
                unmanaged=True,
                ref="aaaaa",
            ),
            PackageNamespaceVersionDependency(namespace="bar", version="2.0"),
            UnmanagedGitHubRefDependency(
                github="https://github.com/SFDO-Tooling/RootRepo",
                subfolder="unpackaged/post/first",
                unmanaged=False,
                ref="aaaaa",
                namespace_inject="bar",
            ),
        ]

    def test_flatten__skip(self, project_config):
        gh = GitHubDynamicDependency(
            github="https://github.com/SFDO-Tooling/RootRepo",
            skip="unpackaged/pre/first",
        )
        gh.ref = "aaaaa"
        gh.managed_dependency = PackageNamespaceVersionDependency(
            namespace="bar", version="2.0"
        )

        assert gh.flatten(project_config) == [
            GitHubDynamicDependency(
                github="https://github.com/SFDO-Tooling/DependencyRepo",
                password_env_name="DEP_PW",
            ),
            UnmanagedGitHubRefDependency(
                github="https://github.com/SFDO-Tooling/RootRepo",
                subfolder="unpackaged/pre/second",
                unmanaged=True,
                ref="aaaaa",
            ),
            PackageNamespaceVersionDependency(namespace="bar", version="2.0"),
            UnmanagedGitHubRefDependency(
                github="https://github.com/SFDO-Tooling/RootRepo",
                subfolder="unpackaged/post/first",
                unmanaged=False,
                ref="aaaaa",
                namespace_inject="bar",
            ),
        ]

    def test_flatten__not_found(self, project_config):
        gh = GitHubDynamicDependency(
            github="https://github.com/SFDO-Tooling/NoUnmanagedPreRepo",
        )
        gh.ref = "aaaaa"
        gh.managed_dependency = PackageNamespaceVersionDependency(
            namespace="foo", version="2.0"
        )

        assert gh.flatten(project_config) == [
            PackageNamespaceVersionDependency(namespace="foo", version="2.0"),
        ]

    def test_flatten__unresolved(self):
        context = mock.Mock()
        gh = GitHubDynamicDependency(repo_owner="Test", repo_name="TestRepo")

        with pytest.raises(DependencyResolutionError) as e:
            gh.flatten(context)

        assert "is not resolved" in str(e)

    def test_flatten__bad_transitive_dep(self, project_config):
        gh = GitHubDynamicDependency(repo_owner="Test", repo_name="RootRepoBadDep")
        gh.ref = "aaaa"

        with pytest.raises(DependencyResolutionError) as e:
            gh.flatten(project_config)

        assert "transitive dependency could not be parsed" in str(e)

    def test_flatten__unmanaged_src(self, project_config):
        gh = GitHubDynamicDependency(
            github="https://github.com/SFDO-Tooling/RootRepo",
            unmanaged=True,
        )
        gh.ref = "aaaaa"

        assert gh.flatten(project_config) == [
            GitHubDynamicDependency(
                github="https://github.com/SFDO-Tooling/DependencyRepo",
                password_env_name="DEP_PW",
            ),
            UnmanagedGitHubRefDependency(
                github="https://github.com/SFDO-Tooling/RootRepo",
                subfolder="unpackaged/pre/first",
                unmanaged=True,
                ref="aaaaa",
            ),
            UnmanagedGitHubRefDependency(
                github="https://github.com/SFDO-Tooling/RootRepo",
                subfolder="unpackaged/pre/second",
                unmanaged=True,
                ref="aaaaa",
            ),
            UnmanagedGitHubRefDependency(
                github="https://github.com/SFDO-Tooling/RootRepo",
                unmanaged=True,
                ref="aaaaa",
            ),
            UnmanagedGitHubRefDependency(
                github="https://github.com/SFDO-Tooling/RootRepo",
                subfolder="unpackaged/post/first",
                unmanaged=True,
                ref="aaaaa",
                namespace_strip="bar",
            ),
        ]

    def test_flatten__no_release(self, project_config):
        gh = GitHubDynamicDependency(
            github="https://github.com/SFDO-Tooling/RootRepo",
            unmanaged=False,
        )
        gh.ref = "aaaaa"

        with pytest.raises(DependencyResolutionError) as e:
            gh.flatten(project_config)

        assert "Could not find latest release" in str(e)

    def test_name(self):
        gh = GitHubDynamicDependency(github="https://github.com/SFDO-Tooling/RootRepo")
        assert gh.github in gh.name


class TestPackageNamespaceVersionDependency:
    @mock.patch(
        "cumulusci.core.dependencies.dependencies.install_package_by_namespace_version"
    )
    def test_install(self, install_package_by_namespace_version):
        m = PackageNamespaceVersionDependency(namespace="test", version="1.0")

        context = mock.Mock()
        org = OrgConfig({}, "dev")
        org._installed_packages = {}

        m.install(context, org)

        install_package_by_namespace_version.assert_called_once_with(
            context,
            org,
            m.namespace,
            m.version,
            PackageInstallOptions(),
            retry_options=DEFAULT_PACKAGE_RETRY_OPTIONS,
        )

    @mock.patch(
        "cumulusci.core.dependencies.dependencies.install_package_by_namespace_version"
    )
    def test_install__already_installed(self, install_package_by_namespace_version):
        m = PackageNamespaceVersionDependency(namespace="test", version="1.0")

        context = mock.Mock()
        org = OrgConfig({}, "dev")
        org._installed_packages = {
            "test": [VersionInfo(id="04t000000000000", number=StrictVersion("1.0"))]
        }

        m.install(context, org)

        install_package_by_namespace_version.assert_not_called()

    @mock.patch(
        "cumulusci.core.dependencies.dependencies.install_package_by_namespace_version"
    )
    def test_install__newer_beta(self, install_package_by_namespace_version):
        m = PackageNamespaceVersionDependency(namespace="test", version="1.1 (Beta 4)")

        context = mock.Mock()
        org = OrgConfig({}, "dev")
        org._installed_packages = {
            "test": [VersionInfo(id="04t000000000000", number=StrictVersion("1.0"))]
        }

        m.install(context, org)

        install_package_by_namespace_version.assert_called_once_with(
            context,
            org,
            m.namespace,
            m.version,
            PackageInstallOptions(),
            retry_options=DEFAULT_PACKAGE_RETRY_OPTIONS,
        )

    @mock.patch(
        "cumulusci.core.dependencies.dependencies.install_package_by_namespace_version"
    )
    def test_install__custom_options(self, install_package_by_namespace_version):
        m = PackageNamespaceVersionDependency(namespace="foo", version="1.0")

        context = mock.Mock()
        org = OrgConfig({}, "dev")
        org._installed_packages = {}
        opts = PackageInstallOptions(password="test")

        m.install(context, org, options=opts)

        install_package_by_namespace_version.assert_called_once_with(
            context,
            org,
            m.namespace,
            m.version,
            opts,
            retry_options=DEFAULT_PACKAGE_RETRY_OPTIONS,
        )

    @mock.patch(
        "cumulusci.core.dependencies.dependencies.install_package_by_namespace_version"
    )
    def test_install__key_from_env(self, install_package_by_namespace_version):
        m = PackageNamespaceVersionDependency(
            namespace="foo", version="1.0", password_env_name="PW"
        )

        context = mock.Mock()
        org = OrgConfig({}, "dev")
        org._installed_packages = {}

        with mock.patch.dict(os.environ, PW="testpw"):
            m.install(context, org)

        opts = install_package_by_namespace_version.call_args[0][4]
        assert opts.password == "testpw"

    def test_name(self):
        assert (
            PackageNamespaceVersionDependency(namespace="foo", version="1.0").name
            == "Install foo 1.0"
        )

    def test_package_name(self):
        assert (
            PackageNamespaceVersionDependency(
                namespace="foo", version="1.0", package_name="Foo"
            ).package
            == "Foo"
        )

        assert (
            PackageNamespaceVersionDependency(namespace="foo", version="1.0").package
            == "foo"
        )


class TestPackageVersionIdDependency:
    @mock.patch(
        "cumulusci.core.dependencies.dependencies.install_package_by_version_id"
    )
    def test_install(self, install_package_by_version_id):
        m = PackageVersionIdDependency(version_id="04t000000000000")

        context = mock.Mock()
        org = OrgConfig({}, "dev")
        org._installed_packages = {}

        m.install(context, org)

        install_package_by_version_id.assert_called_once_with(
            context,
            org,
            m.version_id,
            PackageInstallOptions(),
            retry_options=DEFAULT_PACKAGE_RETRY_OPTIONS,
        )

    @mock.patch(
        "cumulusci.core.dependencies.dependencies.install_package_by_version_id"
    )
    def test_install__already_installed(self, install_package_by_version_id):
        m = PackageVersionIdDependency(version_id="04t000000000000")

        context = mock.Mock()
        org = OrgConfig({}, "dev")
        org._installed_packages = {
            "04t000000000000": [VersionInfo(number="1.0", id="04t000000000000")]
        }

        m.install(context, org)

        install_package_by_version_id.assert_not_called()

    @mock.patch(
        "cumulusci.core.dependencies.dependencies.install_package_by_version_id"
    )
    def test_install__custom_options(self, install_package_by_version_id):
        m = PackageVersionIdDependency(version_id="04t000000000000")

        context = mock.Mock()
        org = OrgConfig({}, "dev")
        org._installed_packages = {}
        opts = PackageInstallOptions(password="test")

        m.install(context, org, options=opts)

        install_package_by_version_id.assert_called_once_with(
            context,
            org,
            m.version_id,
            opts,
            retry_options=DEFAULT_PACKAGE_RETRY_OPTIONS,
        )

    @mock.patch(
        "cumulusci.core.dependencies.dependencies.install_package_by_version_id"
    )
    def test_install__key_from_env(self, install_package_by_version_id):
        m = PackageVersionIdDependency(
            version_id="04t000000000000", password_env_name="PW"
        )

        context = mock.Mock()
        org = OrgConfig({}, "dev")
        org._installed_packages = {}

        with mock.patch.dict(os.environ, PW="testpw"):
            m.install(context, org)

        opts = install_package_by_version_id.call_args[0][3]
        assert opts.password == "testpw"

    def test_name(self):
        assert (
            PackageVersionIdDependency(
                package_name="foo", version_id="04t000000000000"
            ).name
            == "Install foo 04t000000000000"
        )

    def test_package_name(self):
        assert (
            PackageVersionIdDependency(version_id="04t000000000000").package
            == "Unknown Package"
        )


class TestUnmanagedGitHubRefDependency:
    def test_validation(self):
        with pytest.raises(ValidationError):
            UnmanagedGitHubRefDependency(github="http://github.com")

        u = UnmanagedGitHubRefDependency(
            github="https://github.com/Test/TestRepo", ref="aaaaaaaa"
        )

        u = UnmanagedGitHubRefDependency(
            repo_owner="Test", repo_name="TestRepo", ref="aaaaaaaa"
        )
        assert u.github == "https://github.com/Test/TestRepo"

    @mock.patch(
        "cumulusci.core.dependencies.dependencies.download_extract_github_from_repo"
    )
    @mock.patch("cumulusci.core.dependencies.dependencies.MetadataPackageZipBuilder")
    @mock.patch("cumulusci.core.dependencies.dependencies.ApiDeploy")
    def test_install(self, api_deploy_mock, zip_builder_mock, download_mock):
        d = UnmanagedGitHubRefDependency(
            github="http://github.com/Test/TestRepo", ref="aaaaaaaa"
        )

        zf = ZipFile(io.BytesIO(), "w")
        zf.writestr("package.xml", "test")
        download_mock.return_value = zf

        context = mock.Mock()
        org = mock.Mock()
        d.install(context, org)

        download_mock.assert_called_once_with(
            context.get_repo_from_url.return_value, ref=d.ref
        )
        zip_builder_mock.from_zipfile.assert_called_once_with(
            download_mock.return_value,
            path=None,
            options={
                "unmanaged": True,
                "namespace_inject": None,
                "namespace_strip": None,
            },
            logger=mock.ANY,  # the logger
        )
        api_deploy_mock.assert_called_once_with(
            mock.ANY,  # The context object is checked below
            zip_builder_mock.from_zipfile.return_value.as_base64.return_value,
        )
        mock_task = api_deploy_mock.call_args_list[0][0][0]
        assert mock_task.org_config == org
        assert mock_task.project_config == context

        api_deploy_mock.return_value.assert_called_once()

    def test_get_unmanaged(self):
        org = mock.Mock()
        org.installed_packages = {"foo": "1.0"}
        assert (
            UnmanagedGitHubRefDependency(
                github="http://github.com/Test/TestRepo", ref="aaaa", unmanaged=True
            )._get_unmanaged(org)
            is True
        )
        assert (
            UnmanagedGitHubRefDependency(
                github="http://github.com/Test/TestRepo", ref="aaaa"
            )._get_unmanaged(org)
            is True
        )
        assert (
            UnmanagedGitHubRefDependency(
                github="http://github.com/Test/TestRepo",
                ref="aaaa",
                namespace_inject="foo",
            )._get_unmanaged(org)
            is False
        )
        assert (
            UnmanagedGitHubRefDependency(
                github="http://github.com/Test/TestRepo",
                ref="aaaa",
                namespace_inject="bar",
            )._get_unmanaged(org)
            is True
        )

    def test_name(self):
        assert (
            UnmanagedGitHubRefDependency(
                github="http://github.com/Test/TestRepo",
                subfolder="unpackaged/pre/first",
                ref="aaaa",
            ).name
            == "Deploy http://github.com/Test/TestRepo/unpackaged/pre/first"
        )

        assert (
            UnmanagedGitHubRefDependency(
                github="http://github.com/Test/TestRepo",
                ref="aaaa",
            ).name
            == "Deploy http://github.com/Test/TestRepo"
        )


class TestUnmanagedZipURLDependency:
    @mock.patch("cumulusci.core.dependencies.dependencies.download_extract_zip")
    @mock.patch("cumulusci.core.dependencies.dependencies.MetadataPackageZipBuilder")
    @mock.patch("cumulusci.core.dependencies.dependencies.ApiDeploy")
    def test_install(self, api_deploy_mock, zip_builder_mock, download_mock):
        d = UnmanagedZipURLDependency(zip_url="http://foo.com")

        zf = ZipFile(io.BytesIO(), "w")
        zf.writestr("src/package.xml", "test")
        download_mock.return_value = zf

        context = mock.Mock()
        org = mock.Mock()
        d.install(context, org)

        download_mock.assert_called_once_with(d.zip_url)

        zip_builder_mock.from_zipfile.assert_called_once_with(
            mock.ANY,
            options={
                "unmanaged": True,
                "namespace_inject": None,
                "namespace_strip": None,
            },
            path=None,
            logger=mock.ANY,  # the logger
        )
        api_deploy_mock.assert_called_once_with(
            mock.ANY,  # The context object is checked below
            zip_builder_mock.from_zipfile.return_value.as_base64.return_value,
        )
        mock_task = api_deploy_mock.call_args_list[0][0][0]
        assert mock_task.org_config == org
        assert mock_task.project_config == context

        api_deploy_mock.return_value.assert_called_once()

    def test_get_unmanaged(self):
        org = mock.Mock()
        org.installed_packages = {"foo": "1.0"}
        assert (
            UnmanagedZipURLDependency(
                zip_url="http://foo.com", unmanaged=True
            )._get_unmanaged(org)
            is True
        )
        assert (
            UnmanagedZipURLDependency(
                zip_url="http://foo.com", namespace_inject="foo"
            )._get_unmanaged(org)
            is False
        )
        assert (
            UnmanagedZipURLDependency(
                zip_url="http://foo.com", namespace_inject="bar"
            )._get_unmanaged(org)
            is True
        )

    def test_name(self):
        assert (
            UnmanagedZipURLDependency(zip_url="http://foo.com", subfolder="bar").name
            == "Deploy http://foo.com /bar"
        )

    @mock.patch("cumulusci.core.dependencies.dependencies.MetadataPackageZipBuilder")
    @mock.patch("cumulusci.core.dependencies.dependencies.download_extract_zip")
    @mock.patch("cumulusci.core.dependencies.dependencies.zip_subfolder")
    def test_get_metadata_package_zip_builder__mdapi_root(
        self, subfolder_mock, download_zip_mock, zipbuilder_mock
    ):
        zf = ZipFile(io.BytesIO(), "w")
        zf.writestr("src/package.xml", "test")

        dep = UnmanagedZipURLDependency(zip_url="http://foo.com")
        download_zip_mock.return_value = zf
        subfolder_mock.return_value = zip_subfolder(zf, "src")

        context = mock.Mock()
        org = mock.Mock()

        assert (
            dep.get_metadata_package_zip_builder(context, org)
            == zipbuilder_mock.from_zipfile.return_value
        )
        subfolder_mock.assert_called_once_with(zf, "src")
        zipbuilder_mock.from_zipfile.assert_called_once_with(
            subfolder_mock.return_value,
            path=None,
            options={
                "unmanaged": True,
                "namespace_inject": None,
                "namespace_strip": None,
            },
            logger=context.logger,
        )

    @mock.patch("cumulusci.core.dependencies.dependencies.MetadataPackageZipBuilder")
    @mock.patch("cumulusci.core.dependencies.dependencies.download_extract_zip")
    @mock.patch("cumulusci.core.dependencies.dependencies.zip_subfolder")
    def test_get_metadata_package_zip_builder__mdapi_subfolder(
        self, subfolder_mock, download_zip_mock, zipbuilder_mock
    ):
        zf = ZipFile(io.BytesIO(), "w")

        zf.writestr("unpackaged/pre/first/package.xml", "test")

        dep = UnmanagedZipURLDependency(
            zip_url="http://foo.com", subfolder="unpackaged/pre/first"
        )
        download_zip_mock.return_value = zf
        subfolder_mock.return_value = zip_subfolder(zf, "unpackaged/pre/first")
        context = mock.Mock()
        org = mock.Mock()

        assert (
            dep.get_metadata_package_zip_builder(context, org)
            == zipbuilder_mock.from_zipfile.return_value
        )
        subfolder_mock.assert_called_once_with(zf, "unpackaged/pre/first")
        zipbuilder_mock.from_zipfile.assert_called_once_with(
            subfolder_mock.return_value,
            path=None,
            options={
                "unmanaged": True,
                "namespace_inject": None,
                "namespace_strip": None,
            },
            logger=context.logger,
        )

    @mock.patch("cumulusci.core.dependencies.dependencies.MetadataPackageZipBuilder")
    @mock.patch("cumulusci.core.dependencies.dependencies.download_extract_zip")
    @mock.patch("cumulusci.core.dependencies.dependencies.zip_subfolder")
    @mock.patch("cumulusci.core.sfdx.sfdx")
    def test_get_metadata_package_zip_builder__sfdx(
        self, sfdx_mock, subfolder_mock, download_zip_mock, zipbuilder_mock
    ):
        zf = ZipFile(io.BytesIO(), "w")

        zf.writestr("force-app/main/default/classes/", "test")

        dep = UnmanagedZipURLDependency(zip_url="http://foo.com", subfolder="force-app")
        download_zip_mock.return_value = zf

        context = mock.Mock()
        org = mock.Mock()

        assert (
            dep.get_metadata_package_zip_builder(context, org)
            == zipbuilder_mock.from_zipfile.return_value
        )
        subfolder_mock.assert_not_called()
        zipbuilder_mock.from_zipfile.assert_called_once_with(
            None,
            path=mock.ANY,
            options={
                "unmanaged": True,
                "namespace_inject": None,
                "namespace_strip": None,
            },
            logger=context.logger,
        )
        sfdx_mock.assert_called_once_with(
            "force:source:convert",
            args=["-d", mock.ANY, "-r", "force-app"],
            capture_output=True,
            check_return=True,
        )


class TestParseDependency:
    def test_parse_managed_package_dep(self):
        m = parse_dependency({"version": "1.0", "namespace": "foo"})

        assert isinstance(m, PackageNamespaceVersionDependency)

        m = parse_dependency({"version_id": "04t000000000000"})

        assert isinstance(m, PackageVersionIdDependency)

    def test_parse_github_dependency(self):
        g = parse_dependency({"github": "https://github.com/Test/TestRepo"})
        assert isinstance(g, GitHubDynamicDependency)

        g = parse_dependency({"repo_owner": "Test", "repo_name": "TestRepo"})
        assert isinstance(g, GitHubDynamicDependency)

    def test_parse_unmanaged_dependency(self):
        u = parse_dependency(
            {"repo_owner": "Test", "repo_name": "TestRepo", "ref": "aaaaaaaa"}
        )
        assert isinstance(u, UnmanagedGitHubRefDependency)

        u = parse_dependency(
            {"github": "https://github.com/Test/TestRepo", "ref": "aaaaaaaa"}
        )
        assert isinstance(u, UnmanagedGitHubRefDependency)

        u = parse_dependency(
            {
                "zip_url": "https://github.com/Test/TestRepo",
                "subfolder": "unpackaged/pre",
            }
        )
        assert isinstance(u, UnmanagedZipURLDependency)
