from collections import namedtuple
from unittest import mock

import requests_mock
from packaging.specifiers import SpecifierSet

from app_utils.testing import NoSocketsTestCase

from package_monitor.core import (
    DistributionPackage,
    compile_package_requirements,
    dist_metadata_value,
    gather_distribution_packages,
    update_packages_from_pypi,
)

from .factories import (
    DistributionPackageFactory,
    DjangoAppConfigStub,
    ImportlibDistributionStubFactory,
    PypiFactory,
    PypiReleaseFactory,
    make_packages_container,
)

MODULE_PATH = "package_monitor.core"

SysVersionInfo = namedtuple("SysVersionInfo", ["major", "minor", "micro"])


class TestDistributionPackage(NoSocketsTestCase):
    @mock.patch(MODULE_PATH + ".django_apps", spec=True)
    def test_should_create_from_importlib_distribution(self, mock_django_apps):
        # given
        dist = ImportlibDistributionStubFactory(
            name="Alpha",
            version="1.2.3",
            requires=["bravo>=1.0.0"],
            files=["alpha/__init__.py"],
            homepage_url="https://www.alpha.com",
        )
        mock_django_apps.get_app_configs.return_value = [
            DjangoAppConfigStub("alpha_app", "/alpha/__init__.py")
        ]
        # when
        obj = DistributionPackage.create_from_distribution(dist)
        # then
        self.assertEqual(obj.name, "Alpha")
        self.assertEqual(obj.name_normalized, "alpha")
        self.assertEqual(obj.current, "1.2.3")
        self.assertEqual(obj.latest, "")
        self.assertListEqual([str(x) for x in obj.requirements], ["bravo>=1.0.0"])
        self.assertEqual(obj.apps, ["alpha_app"])
        self.assertEqual(obj.homepage_url, "https://www.alpha.com")

    def test_should_not_be_editable(self):
        # given
        obj = DistributionPackageFactory()
        # when/then
        self.assertFalse(obj.is_editable())

    @mock.patch(MODULE_PATH + ".os.path.isfile")
    def test_should_be_editable(self, mock_isfile):
        # given
        mock_isfile.return_value = True
        obj = DistributionPackageFactory()
        # when/then
        self.assertTrue(obj.is_editable())


@mock.patch(MODULE_PATH + ".importlib_metadata.distributions", spec=True)
class TestFetchRelevantPackages(NoSocketsTestCase):
    def test_should_fetch_all_packages(self, mock_distributions):
        # given
        dist_alpha = ImportlibDistributionStubFactory(name="alpha")
        dist_bravo = ImportlibDistributionStubFactory(
            name="bravo", requires=["alpha>=1.0.0"]
        )
        distributions = lambda: iter([dist_alpha, dist_bravo])  # noqa: E731
        mock_distributions.side_effect = distributions
        # when
        result = gather_distribution_packages()
        # then
        self.assertSetEqual({"alpha", "bravo"}, set(result.keys()))


class TestCompilePackageRequirements(NoSocketsTestCase):
    def test_should_compile_requirements(self):
        # given
        dist_alpha = DistributionPackageFactory(name="alpha")
        dist_bravo = DistributionPackageFactory(name="bravo", requires=["alpha>=1.0.0"])
        packages = make_packages_container([dist_alpha, dist_bravo])
        # when
        result = compile_package_requirements(packages)
        # then
        expected = {"alpha": {"bravo": SpecifierSet(">=1.0.0")}}
        self.assertDictEqual(expected, result)

    def test_should_ignore_invalid_requirements(self):
        # given
        dist_alpha = DistributionPackageFactory(name="alpha")
        dist_bravo = DistributionPackageFactory(name="bravo", requires=["alpha>=1.0.0"])
        dist_charlie = DistributionPackageFactory(name="charlie", requires=["123"])
        packages = make_packages_container([dist_alpha, dist_bravo, dist_charlie])
        # when
        result = compile_package_requirements(packages)
        # then
        expected = {"alpha": {"bravo": SpecifierSet(">=1.0.0")}}
        self.assertDictEqual(expected, result)

    # def test_should_ignore_python_version_requirements(self):
    #     # given
    #     dist_alpha = DistributionPackageFactory(name="alpha")
    #     dist_bravo = DistributionPackageFactory(name="bravo", requires=["alpha>=1.0.0"])
    #     dist_charlie = DistributionPackageFactory(
    #         name="charlie", requires=["alpha >= 1.0.0 ; python_version < 3.7"]
    #     )
    #     packages = make_packages_container([dist_alpha, dist_bravo, dist_charlie])
    #     # when
    #     result = compile_package_requirements(packages)
    #     # then
    #     expected = {"alpha": {"bravo": SpecifierSet(">=1.0.0")}}
    #     self.assertDictEqual(expected, result)

    def test_should_ignore_invalid_extra_requirements(self):
        # given
        dist_alpha = DistributionPackageFactory(name="alpha")
        dist_bravo = DistributionPackageFactory(name="bravo", requires=["alpha>=1.0.0"])
        dist_charlie = DistributionPackageFactory(
            name="charlie", requires=['alpha>=1.0.0; extra == "certs"']
        )
        packages = make_packages_container([dist_alpha, dist_bravo, dist_charlie])
        # when
        result = compile_package_requirements(packages)
        # then
        expected = {"alpha": {"bravo": SpecifierSet(">=1.0.0")}}
        self.assertDictEqual(expected, result)


@requests_mock.Mocker()
class TestUpdatePackagesFromPyPi(NoSocketsTestCase):
    def test_should_update_packages(self, requests_mocker):
        # given
        dist_alpha = DistributionPackageFactory(name="alpha", current="1.0.0")
        packages = make_packages_container([dist_alpha])
        requirements = {}
        pypi_alpha = PypiFactory(distribution=dist_alpha)
        pypi_alpha.releases["1.1.0"] = [PypiReleaseFactory()]
        requests_mocker.register_uri(
            "GET", "https://pypi.org/pypi/alpha/json", json=pypi_alpha.asdict()
        )
        # when
        update_packages_from_pypi(
            packages=packages, requirements=requirements, use_threads=False
        )
        # then
        self.assertEqual(packages["alpha"].latest, "1.1.0")

    def test_should_ignore_prereleases_when_stable(self, requests_mocker):
        # given
        dist_alpha = DistributionPackageFactory(name="alpha", current="1.0.0")
        packages = make_packages_container([dist_alpha])
        requirements = {}
        pypi_alpha = PypiFactory(distribution=dist_alpha)
        pypi_alpha.releases["1.1.0a1"] = [PypiReleaseFactory()]
        requests_mocker.register_uri(
            "GET", "https://pypi.org/pypi/alpha/json", json=pypi_alpha.asdict()
        )
        # when
        update_packages_from_pypi(
            packages=packages, requirements=requirements, use_threads=False
        )
        # then
        self.assertEqual(packages["alpha"].latest, "1.0.0")

    def test_should_include_prereleases_when_prerelease(self, requests_mocker):
        # given
        dist_alpha = DistributionPackageFactory(name="alpha", current="1.0.0a1")
        packages = make_packages_container([dist_alpha])
        requirements = {}
        pypi_alpha = PypiFactory(distribution=dist_alpha)
        pypi_alpha.releases["1.0.0a2"] = [PypiReleaseFactory()]
        requests_mocker.register_uri(
            "GET", "https://pypi.org/pypi/alpha/json", json=pypi_alpha.asdict()
        )
        # when
        update_packages_from_pypi(
            packages=packages, requirements=requirements, use_threads=False
        )
        # then
        self.assertEqual(packages["alpha"].latest, "1.0.0a2")

    def test_should_set_latest_to_empty_string_on_network_error(self, requests_mocker):
        # given
        dist_alpha = DistributionPackageFactory(name="alpha", current="1.0.0")
        packages = make_packages_container([dist_alpha])
        requirements = {}
        pypi_alpha = PypiFactory(distribution=dist_alpha)
        pypi_alpha.releases["1.1.0"] = [PypiReleaseFactory()]
        requests_mocker.register_uri(
            "GET",
            "https://pypi.org/pypi/alpha/json",
            status_code=500,
            reason="Test error",
        )
        # when
        update_packages_from_pypi(
            packages=packages, requirements=requirements, use_threads=False
        )
        # then
        self.assertEqual(packages["alpha"].latest, "")

    def test_should_ignore_yanked_releases(self, requests_mocker):
        # given
        dist_alpha = DistributionPackageFactory(name="alpha", current="1.0.0")
        packages = make_packages_container([dist_alpha])
        requirements = {}
        pypi_alpha = PypiFactory(distribution=dist_alpha)
        pypi_alpha.releases["1.1.0"] = [PypiReleaseFactory(yanked=True)]
        requests_mocker.register_uri(
            "GET", "https://pypi.org/pypi/alpha/json", json=pypi_alpha.asdict()
        )
        # when
        update_packages_from_pypi(
            packages=packages, requirements=requirements, use_threads=False
        )
        # then
        self.assertEqual(packages["alpha"].latest, "1.0.0")

    @mock.patch(MODULE_PATH + ".sys")
    def test_should_ignore_releases_with_incompatible_python_requirement(
        self, requests_mocker, mock_sys
    ):
        # given
        mock_sys.version_info = SysVersionInfo(3, 6, 9)
        dist_alpha = DistributionPackageFactory(name="alpha", current="1.0.0")
        packages = make_packages_container([dist_alpha])
        requirements = {}
        pypi_alpha = PypiFactory(distribution=dist_alpha)
        pypi_alpha.releases["1.1.0"] = [PypiReleaseFactory(requires_python=">=3.7")]
        requests_mocker.register_uri(
            "GET", "https://pypi.org/pypi/alpha/json", json=pypi_alpha.asdict()
        )
        # when
        update_packages_from_pypi(
            packages=packages, requirements=requirements, use_threads=False
        )
        # then
        self.assertEqual(packages["alpha"].latest, "1.0.0")


class TestDistMetadataValue(NoSocketsTestCase):
    def test_should_return_value_when_exists(self):
        # given
        dist = ImportlibDistributionStubFactory(name="Alpha")
        # when/then
        self.assertEqual(dist_metadata_value(dist, "Name"), "Alpha")

    def test_should_raise_error_when_prop_does_not_exist(self):
        # given
        dist = ImportlibDistributionStubFactory(name="Alpha")
        # when/then
        with self.assertRaises(KeyError):
            dist_metadata_value(dist, "XXX")

    def test_should_return_name(self):
        # given
        dist = ImportlibDistributionStubFactory(name="Alpha")
        # when/then
        self.assertEqual(dist_metadata_value(dist, "Name"), "Alpha")

    def test_should_return_empty_string_when_value_is_undefined(self):
        # given
        dist = ImportlibDistributionStubFactory(homepage_url="")
        # when/then
        self.assertEqual(dist_metadata_value(dist, "Home-page"), "")

    def test_should_return_empty_string_when_value_is_none(self):
        # given
        dist = ImportlibDistributionStubFactory(homepage_url=None)
        # when/then
        self.assertEqual(dist_metadata_value(dist, "Home-page"), "")
