from importlib.metadata import PackageNotFoundError
from importlib.metadata import version
from Products.CMFCore.utils import getToolByName
from Products.CMFPlone.factory import _DEFAULT_PROFILE
from Products.CMFPlone.tests import PloneTestCase
from zope.component import queryUtility

import unittest

try:
    HAS_PLONE_APP_UPGRADE_DEV = "dev" in version("plone.app.upgrade")
    HAS_PLONE_APP_UPGRADE = True
except PackageNotFoundError:
    HAS_PLONE_APP_UPGRADE_DEV = False
    HAS_PLONE_APP_UPGRADE = False

# Python 3 is only supported on 5.2+.
# This means you can not upgrade from 5.1 or earlier.
START_PROFILE = "5200"


class TestMigrationTool(PloneTestCase.PloneTestCase):
    def afterSetUp(self):
        self.migration = getToolByName(self.portal, "portal_migration")
        self.setup = getToolByName(self.portal, "portal_setup")

    def testMigrationFinished(self):
        self.assertEqual(
            self.migration.getInstanceVersion(),
            self.migration.getFileSystemVersion(),
            "Migration failed",
        )

    def testMigrationNeedsUpgrading(self):
        self.assertFalse(self.migration.needUpgrading(), "Migration needs upgrading")

    def testMigrationNeedsUpdateRole(self):
        self.assertFalse(self.migration.needUpdateRole(), "Migration needs role update")

    def testMigrationNeedsRecatalog(self):
        self.assertFalse(self.migration.needRecatalog(), "Migration needs recataloging")

    def testListSetupUpgradeSteps(self):
        # There should be no upgrade steps from the current version
        upgrades = self.setup.listUpgrades(_DEFAULT_PROFILE)
        self.assertEqual(len(upgrades), 0)

    def testListOwnUpgradeSteps(self):
        # There should be no upgrade steps from the current version
        upgrades = self.migration.listUpgrades()
        self.assertEqual(len(upgrades), 0)

    def test_profile(self):
        self.assertEqual(self.migration.get_profile(), "Products.CMFPlone:plone")

    def test_package_name(self):
        self.assertEqual(self.migration.get_package_name(), "Products.CMFPlone")

    def test_getInstanceVersion(self):
        version = int(self.migration.getInstanceVersion())
        self.assertGreaterEqual(version, 6201)
        self.assertLessEqual(version, 9000)

    def test_getFileSystemVersion(self):
        version = int(self.migration.getFileSystemVersion())
        self.assertGreaterEqual(version, 6201)
        self.assertLessEqual(version, 9000)

    def test_getSoftwareVersion(self):
        from packaging.version import parse

        version = self.migration.getSoftwareVersion()
        parsed_version = parse(version)
        self.assertGreaterEqual(parsed_version, parse("6.2.0a1"))
        self.assertLessEqual(parsed_version, parse("9.0.0a1"))

    def test_other_base_profile(self):
        orig_context_id = self.setup.getBaselineContextID()
        plone_instance_version = self.migration.getInstanceVersion()
        plone_fs_version = self.migration.getFileSystemVersion()
        plone_software_version = self.migration.getSoftwareVersion()
        try:
            self.setup.setBaselineContext("PluggableAuthService:simple")
            self.assertEqual(
                self.migration.get_profile(), "PluggableAuthService:simple"
            )
            self.assertEqual(self.migration.get_package_name(), "PluggableAuthService")
            # Currently, the next two versions are 1.6.1 (from metadata.xml in
            # PluggableAuthService), but that can change.  The two versions
            # should be the same though, and not unknown.
            pas_instance_version = self.migration.getInstanceVersion()
            pas_fs_version = self.migration.getFileSystemVersion()
            self.assertTrue(pas_instance_version)
            self.assertNotEqual(pas_instance_version, "unknown")
            self.assertEqual(pas_instance_version, pas_fs_version)
            self.assertNotEqual(pas_instance_version, plone_instance_version)
            self.assertNotEqual(pas_fs_version, plone_fs_version)
            # PluggableAuthService manually registers its base profile
            # with the name PluggableAuthService:simple.  This means that
            # the package_name that we compute from the profile is
            # PluggableAuthService, and not Products.PluggableAuthService,
            # so we cannot actually find a package.  For normally named
            # profiles and packages this should be no problem.
            # In the case here, we are happy if that the method does not fail.
            pas_software_version = self.migration.getSoftwareVersion()
            self.assertNotEqual(pas_software_version, plone_software_version)
            self.assertIsNone(pas_software_version)
        finally:
            self.setup.setBaselineContext(orig_context_id)

    @unittest.skipUnless(
        HAS_PLONE_APP_UPGRADE_DEV, reason="Only run with plone.app.upgrade checkouts"
    )
    def testDoUpgrades(self):
        self.setRoles(["Manager"])
        self.setup.setLastVersionForProfile(_DEFAULT_PROFILE, START_PROFILE)
        upgrades = self.migration.listUpgrades()
        self.assertGreater(len(upgrades), 0)

        request = self.portal.REQUEST
        request.form["profile_id"] = _DEFAULT_PROFILE

        steps = []
        for upgrade in upgrades:
            if isinstance(upgrade, list):
                steps.extend([s["id"] for s in upgrade])
            else:
                steps.append(upgrade["id"])

        request.form["upgrades"] = steps
        self.setup.manage_doUpgrades(request=request)

        # And we have reached our current profile version
        current = self.setup.getVersionForProfile(_DEFAULT_PROFILE)
        current = tuple(current.split("."))
        last = self.setup.getLastVersionForProfile(_DEFAULT_PROFILE)
        self.assertEqual(last, current)

        # There are no more upgrade steps available
        upgrades = self.migration.listUpgrades()
        self.assertEqual(len(upgrades), 0)

    @unittest.skipUnless(
        HAS_PLONE_APP_UPGRADE_DEV,
        reason="plone.app.upgrade not installed",
    )
    def testUpgrade(self):
        self.setRoles(["Manager"])
        self.setup.setLastVersionForProfile(_DEFAULT_PROFILE, START_PROFILE)
        self.migration.upgrade()

        # And we have reached our current profile version
        current = self.setup.getVersionForProfile(_DEFAULT_PROFILE)
        current = tuple(current.split("."))
        last = self.setup.getLastVersionForProfile(_DEFAULT_PROFILE)
        self.assertEqual(last, current)

        # There are no more upgrade steps available
        upgrades = self.migration.listUpgrades()
        self.assertEqual(len(upgrades), 0)


class TestMigrationWithExtraUpgrades(PloneTestCase.PloneTestCase):
    """Test a migration with a too new upgrade available.

    There should be no upgrade steps newer than the current version.
    If our FS profile version is 5, and there is an upgrade to 6,
    we do not want to see it.  This just means we have a newer
    plone.app.upgrade, which is fine.
    """

    def afterSetUp(self):
        from Products.GenericSetup.upgrade import _registerUpgradeStep
        from Products.GenericSetup.upgrade import UpgradeStep

        self.migration = getToolByName(self.portal, "portal_migration")
        self.setup = getToolByName(self.portal, "portal_setup")

        def failing_upgrade(context):
            raise AssertionError("Too new upgrade should not be run!")

        # Register a too new upgrade.
        fs_version = self.migration.getFileSystemVersion()
        new_version = str(int(fs_version) + 1)
        new_step = UpgradeStep(
            "Too new upgrade",
            _DEFAULT_PROFILE,
            fs_version,
            new_version,
            "",
            failing_upgrade,
            None,
            "1",
        )
        self.step_id = new_step.id
        _registerUpgradeStep(new_step)

    def beforeTearDown(self):
        # Remove the extra step from the upgrade registry,
        # otherwise this bleeds over into other tests.
        from Products.GenericSetup.upgrade import _upgrade_registry

        profile_steps = _upgrade_registry.getUpgradeStepsForProfile(_DEFAULT_PROFILE)
        profile_steps.pop(self.step_id, None)

    def testListUpgradeStepsNotTooNew(self):
        # portal_setup happily reports the newer upgrade
        upgrades = self.setup.listUpgrades(_DEFAULT_PROFILE)
        self.assertGreater(len(upgrades), 0)
        # Our migration tool no longer shows it.
        upgrades = self.migration.listUpgrades()
        self.assertEqual(len(upgrades), 0)

    @unittest.skipUnless(
        HAS_PLONE_APP_UPGRADE_DEV,
        reason="plone.app.upgrade not installed",
    )
    def testUpgrade(self):
        self.setRoles(["Manager"])
        self.setup.setLastVersionForProfile(_DEFAULT_PROFILE, START_PROFILE)
        self.migration.upgrade()

        # And we have reached our current profile version
        current = self.setup.getVersionForProfile(_DEFAULT_PROFILE)
        current = tuple(current.split("."))
        last = self.setup.getLastVersionForProfile(_DEFAULT_PROFILE)
        self.assertEqual(last, current)

        # There are no more upgrade steps available
        upgrades = self.migration.listUpgrades()
        self.assertEqual(len(upgrades), 0)


class TestAddonList(PloneTestCase.PloneTestCase):
    def test_addon_safe(self):
        from Products.CMFPlone.MigrationTool import Addon

        addon = Addon(profile_id=_DEFAULT_PROFILE)
        self.assertTrue(addon.safe())
        addon = Addon(profile_id=_DEFAULT_PROFILE, check_module="Products.CMFPlone")
        self.assertTrue(addon.safe())
        addon = Addon(
            profile_id=_DEFAULT_PROFILE, check_module="Products.CMFPlone.foobarbaz"
        )
        self.assertFalse(addon.safe())

    def test_addon_repr(self):
        from Products.CMFPlone.MigrationTool import Addon

        addon = Addon(profile_id="foo")
        self.assertEqual(repr(addon), "<Addon profile foo>")
        self.assertEqual(str(addon), "<Addon profile foo>")

    def test_upgrade_all(self):
        from Products.CMFPlone.MigrationTool import Addon
        from Products.CMFPlone.MigrationTool import AddonList

        # real ones:
        cmfeditions = Addon(profile_id="Products.CMFEditions:CMFEditions")
        pae = Addon(profile_id="plone.app.event:default")
        # real one with failing check_module:
        dexterity = Addon(
            profile_id="plone.app.dexterity:default", check_module="no.such.module"
        )
        # non-existing one:
        foo = Addon(profile_id="foo")
        addonlist = AddonList([cmfeditions, pae, dexterity, foo])
        # Calling it should give no errors.
        addonlist.upgrade_all(self.portal)

        # Get the last CMFEditions profile version, as that will be
        # the only one that gets upgraded during the second upgrade.
        setup = getToolByName(self.portal, "portal_setup")
        cmfeditions_version = setup.getLastVersionForProfile(cmfeditions.profile_id)

        # Now mess with the profile versions.
        setup.setLastVersionForProfile(cmfeditions.profile_id, "2.0")
        setup.setLastVersionForProfile(dexterity.profile_id, "0.1")
        # 'unknown' needs special handling, otherwise the version will
        # become a tuple ('unknown',):
        setup._profile_upgrade_versions[pae.profile_id] = "unknown"

        # Run the upgrade again.
        addonlist.upgrade_all(self.portal)

        # Check the profile versions.
        # CMFEditions should be the last one:
        self.assertEqual(
            setup.getLastVersionForProfile(cmfeditions.profile_id), cmfeditions_version
        )
        # We had set pae to unknown, so it will not have been
        # upgraded:
        self.assertEqual(setup.getLastVersionForProfile(pae.profile_id), "unknown")
        # We had given dexterity a failing check_module, so it will
        # not have been upgraded:
        self.assertEqual(
            setup.getLastVersionForProfile(dexterity.profile_id), ("0", "1")
        )
        # The foo profile never existed:
        self.assertEqual(setup.getLastVersionForProfile(foo.profile_id), "unknown")

    def test_plone_addonlist_upgrade_all(self):
        # Test the actual filled addon list.
        from Products.CMFPlone.MigrationTool import ADDON_LIST

        # Several addons did not get fully upgraded in the past, which
        # is why this list was created.
        cmfeditions_id = "Products.CMFEditions:CMFEditions"
        querystring_id = "plone.app.querystring:default"
        # Note the current versions.
        setup = getToolByName(self.portal, "portal_setup")
        getversion = setup.getLastVersionForProfile
        cmfeditions_version = getversion(cmfeditions_id)
        querystring_version = getversion(querystring_id)
        # Check that they are not unknown
        self.assertNotEqual(cmfeditions_version, "unknown")
        self.assertNotEqual(querystring_version, "unknown")
        # So let's mess with some profile versions.  We get some older
        # versions that really exist.
        setversion = setup.setLastVersionForProfile
        setversion(cmfeditions_id, "2.0")
        setversion(querystring_id, "7")
        # Check that it worked, that the profile versions really are
        # different.
        self.assertNotEqual(cmfeditions_version, getversion(cmfeditions_id))
        self.assertNotEqual(querystring_version, getversion(querystring_id))

        # Run the upgrade.
        ADDON_LIST.upgrade_all(self.portal)

        # Check that it worked, that the profiles are now at their
        # original versions.
        self.assertEqual(cmfeditions_version, getversion(cmfeditions_id))
        self.assertEqual(querystring_version, getversion(querystring_id))

    def test_addonlist_utility(self):
        from Products.CMFPlone.MigrationTool import AddonList
        from Products.CMFPlone.MigrationTool import IAddonList

        util = queryUtility(IAddonList, "Products.CMFPlone")
        self.assertIsNotNone(util)
        self.assertTrue(hasattr(util, "addon_list"))
        self.assertTrue(hasattr(util, "pre_addon_list"))
        self.assertIsInstance(util.addon_list, AddonList)
        self.assertGreater(len(util.addon_list), 7)
        self.assertIsInstance(util.pre_addon_list, AddonList)
        self.assertEqual(len(util.pre_addon_list), 0)


class TestPloneUpgradePage(PloneTestCase.PloneTestCase):
    def afterSetUp(self):
        self.migration = getToolByName(self.portal, "portal_migration")
        self.setup = getToolByName(self.portal, "portal_setup")

    @unittest.skipUnless(
        HAS_PLONE_APP_UPGRADE,
        reason="plone.app.upgrade not installed",
    )
    def test_upgrades(self):
        self.setRoles(["Manager"])
        view = self.portal.restrictedTraverse("@@plone-upgrade")
        self.assertEqual(view.upgrades(), [])
        self.setup.setLastVersionForProfile(_DEFAULT_PROFILE, START_PROFILE)
        self.assertGreater(len(view.upgrades()), 0)

    def test_missing_packages(self):
        self.setRoles(["Manager"])
        view = self.portal.restrictedTraverse("@@plone-upgrade")
        self.assertEqual(view.missing_packages, [])

        # Fake a missing package that was installed.
        self.setup.setLastVersionForProfile("my.dummy.package:default", 1)
        # Delete the cached property.
        del view.missing_packages
        self.assertEqual(view.missing_packages, ["my.dummy.package"])
