# -*- coding: utf-8 -*-

# Copyright (C) 2012-2018 Red Hat, Inc.
#
# This copyrighted material is made available to anyone wishing to use,
# modify, copy, or redistribute it subject to the terms and conditions of
# the GNU General Public License v.2, or (at your option) any later version.
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY expressed or implied, including the implied warranties of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
# Public License for more details.  You should have received a copy of the
# GNU General Public License along with this program; if not, write to the
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.  Any Red Hat trademarks that are incorporated in the
# source code or documentation are not subject to the GNU General Public
# License and may only be used or replicated with the express permission of
# Red Hat, Inc.
#

from __future__ import absolute_import
from __future__ import unicode_literals

import itertools
import logging

import dnf.exceptions

import tests.support


class CommonTest(tests.support.ResultTestCase):

    """Tests common to any 'multilib_policy' and 'best'.

    The test fixture consists of a dnf.Base instance that:

    - contains a package "lotus-3-17.x86_64" (The package can be installed.)
    - contains a package "lotus-3-17.i686" (The package can be installed.)
    - contains a package "trampoline-2.1-1.noarch" that contains
      "/all/be/there", provides "splendid > 2.0" and "php(a/b)" (The package
      can be installed.)
    - contains a package "mrkite-2-0.x86_64" (The package can be installed
      together with the package "trampoline".)
    - contains a package "mrkite-k-h-1-1.x86_64" (The package can be
      installed.)
    - contains a package "pepper-20-0.src"
    - contains a package "pepper-20-2.x86_64" (The package cannot be
      installed.)
    - contains a package "librita-1-1.x86_64" (The package is already
      installed.)
    - contains a package "hole-1-2.x86_64" (The package can be installed as an
      upgrade.)

    """

    REPOS = ['main', 'third_party', 'broken_deps']

    def test_install_arch_glob(self):
        """Test that the pkg specification can contain an architecture glob."""
        self.base.install("lotus.*6*")
        installed = self.installed_removed(self.base)[0]
        self.assertCountEqual(map(str, installed),
                              ['lotus-3-17.i686',
                               'lotus-3-17.x86_64'])

    def test_install_filename_glob(self):
        """Test that the pkg to be installed can be specified by fname glob."""
        self.base.install("*/there")
        (installed, _) = self.installed_removed(self.base)
        self.assertCountEqual(map(str, installed),
                              ('trampoline-2.1-1.noarch',))

        self.base.install("/all/*/there")
        (installed, _) = self.installed_removed(self.base)
        self.assertCountEqual(map(str, installed),
                              ('trampoline-2.1-1.noarch',))

    def test_install_name(self):
        """Test that the package to be installed can be specified by name."""
        self.base.install("mrkite")
        available = self.base.sack.query().available()
        expected = available.filter(name=["mrkite", "trampoline"]).run()
        # ensure sanity of the test (otherwise it would pass no matter what):
        self.assertEqual(len(expected), 2)
        new_set = self.base.sack.query().installed() + expected
        self.assertResult(self.base, new_set)

    def test_install_name_glob(self):
        """Test that the pkg to be installed can be specified by name glob."""
        self.base.install("mrkite*")
        installed = self.installed_removed(self.base)[0]
        self.assertCountEqual(map(str, installed),
                              ['mrkite-2-0.x86_64',
                               'mrkite-k-h-1-1.x86_64',
                               'trampoline-2.1-1.noarch'])

    def test_install_name_glob_exclude(self):
        """Test that glob excludes play well with glob installs."""
        subj_ex = dnf.subject.Subject('*-1')
        pkgs_ex = subj_ex.get_best_query(self.base.sack)
        self.base.sack.add_excludes(pkgs_ex)

        self.base.install('mrkite*')
        (installed, _) = self.installed_removed(self.base)
        self.assertCountEqual(map(str, installed),
                              ['mrkite-2-0.x86_64',
                               'trampoline-2.1-1.noarch'])

    def test_install_nevra(self):
        """Test that the package to be installed can be specified by NEVRA."""
        self.base.install("lotus-3-17.i686")
        lotus, = dnf.subject.Subject('lotus-3-17.i686').get_best_query(self.base.sack)
        new_set = self.base.sack.query().installed() + [lotus]
        self.assertResult(self.base, new_set)

    def test_install_provide_slash(self):
        self.base.install("php(a/b)")
        (installed, _) = self.installed_removed(self.base)
        self.assertCountEqual(map(str, installed),
                              ('trampoline-2.1-1.noarch',))

    def test_install_provide_version(self):
        """Test that the pkg to be installed can be spec. by provide ver."""
        self.base.install('splendid > 2.0')
        self.assertGreater(self.base._goal.req_length(), 0)
        trampoline = self.base.sack.query().available().filter(
            name="trampoline")
        new_set = self.base.sack.query().installed() + trampoline.run()
        self.assertResult(self.base, new_set)

    def test_package_install_conflict(self):
        """Test that a conflicting package cannot be installed."""
        p = self.base.sack.query().available().filter(
            nevra='pepper-20-2.x86_64')[0]
        self.assertEqual(1, self.base.package_install(p))
        with self.assertRaises(dnf.exceptions.DepsolveError):
            self.base.resolve()

    def test_package_install_installed(self):
        """Test that nothing changes if an installed package matches."""
        p = self.base.sack.query().available()._nevra("librita-1-1.x86_64")[0]
        with tests.support.mock.patch('logging.Logger.warning'):
            self.base.package_install(p)
        self.base.resolve()
        self.assertEmpty(self.base._goal.list_reinstalls())

        self.base.history.close()

        self.base.package_reinstall(p)
        self.base.resolve()
        self.assertLength(self.base._goal.list_reinstalls(), 1)

    def test_package_install_upgrade(self):
        """Test that the pkg to be installed can be an upgrade."""
        p = self.base.sack.query().available().filter(
            nevra='hole-1-2.x86_64')[0]
        self.assertEqual(1, self.base.package_install(p))
        installed, removed = self.installed_removed(self.base)
        self.assertIn(p, installed)
        self.assertGreaterEqual(
            removed,
            set(self.base.sack.query().installed().filter(name='hole')))

    def test_pkg_install_installable(self):
        """Test that the package to be installed can be a package instance."""
        p = self.base.sack.query().available().filter(
            nevra='lotus-3-17.x86_64')[0]
        self.assertEqual(1, self.base.package_install(p))
        self.base.resolve()
        self.assertEqual(1, len(self.base._goal.list_installs()))

    def test_pkg_install_installonly(self):
        """Test that installonly packages are installed, not upgraded."""
        self.base.conf.installonlypkgs += ['hole']
        p = self.base.sack.query().available().filter(
            nevra='hole-1-2.x86_64')[0]
        self.assertEqual(1, self.base.package_install(p))
        installed, removed = self.installed_removed(self.base)
        self.assertIn(p, installed)
        self.assertFalse(
            set(removed) &
            set(self.base.sack.query().installed().filter(name='hole')))


class MultilibAllTest(tests.support.ResultTestCase):

    """Tests for multilib_policy='all'.

    The test fixture consists of a dnf.Base instance that:

    - has conf.multilib_policy == "all"
    - has conf.best == False
    - contains a package "pepper-20-2" (The package cannot be installed.)
    - contains a package "lotus-3-16.x86_64" that contains "/usr/lib*/liblot*"
      in a "main" repository (The package can be installed.)
    - contains a package "lotus-3-16.i686" that contains "/usr/lib*/liblot*" in
      the "main" repository (The package can be installed.)
    - contains a package "librita-1-1.x86_64" (The package is already
      installed.)
    - contains a package "hole-1-2.x86_64" (The package can be installed as an
      upgrade.)
    - contains a package "pepper-20-0.src"
    - contains a package "pepper-20-0.x86_64" (The package is already
      installed.)
    - contains nothing that matches "not-available"
    - contains a package that provides "henry(the_horse)" (The package can be
      installed.)
    - contains a package "lotus-3-17.x86_64" not in a "main" repository (The
      package can be installed.)
    - contains a package "lotus-3-17.i686" not in the "main" repository (The
      package can be installed.)

    """

    REPOS = ['main', 'third_party', 'broken_deps']

    def setUp(self):
        super(MultilibAllTest, self).setUp()
        self.base.conf.multilib_policy = "all"
        assert self.base.conf.best is False

    def test_install_conflict(self):
        """Test that the exception is raised if the package conflicts."""
        self.base.install('pepper-20-2')
        with self.assertRaises(dnf.exceptions.DepsolveError):
            self.base.resolve()

    def test_install_filename(self):
        """Test that the pkg to be installed can be specified by filename."""
        self.base.install("/usr/lib*/liblot*")
        inst, _ = self.installed_removed(self.base)
        self.assertCountEqual(
            map(str, inst), ['lotus-3-16.x86_64', 'lotus-3-16.i686'])

    def test_install_installed(self):
        """Test that nothing changes if an installed package matches."""
        stdout = dnf.pycomp.StringIO()
        with tests.support.wiretap_logs('dnf', logging.INFO, stdout):
            self.base.install('librita')
        self.assertEqual(self.base._goal.req_length(), 0)
        self.assertIn(
            'Package librita-1-1.x86_64 is already installed.',
            stdout.getvalue())

    def test_install_installonly(self):
        """Test that installonly packages are installed, not upgraded."""
        self.base.conf.installonlypkgs += ['hole']
        self.base.install('hole-1-2')
        installed, removed = self.installed_removed(self.base)
        self.assertGreaterEqual(
            installed,
            set(self.base.sack.query().available()._nevra('hole-1-2.x86_64')))
        self.assertEmpty(removed)

    def test_install_multilib(self):
        """Test that pkgs for all architectures are installed if available."""
        cnt = self.base.install("lotus")
        self.assertEqual(cnt, 2)
        installed = self.installed_removed(self.base)[0]
        self.assertLessEqual(
            set(installed),
            set(self.base.sack.query().available().filter(name='lotus')))

    def test_install_name_choice(self):
        """Test that the matching pkg that can be installed is installed."""
        # Don't install the SRPM.
        self.base.sack.add_excludes(
            dnf.subject.Subject('pepper.src').get_best_query(self.base.sack))

        stdout = dnf.pycomp.StringIO()
        with tests.support.wiretap_logs('dnf', logging.INFO, stdout):
            self.base.install('pepper')
        self.assertEqual(self.base._goal.req_length(), 0)
        self.assertIn(
            'Package pepper-20-0.x86_64 is already installed.',
            stdout.getvalue())

    def test_install_nonexistent(self):
        """Test that the exception is raised if no package matches."""
        with self.assertRaises(dnf.exceptions.MarkingError) as context:
            self.base.install('not-available')
        self.assertEqual(context.exception.pkg_spec, 'not-available')
        installed_pkgs = self.base.sack.query().installed().run()
        self.assertResult(self.base, installed_pkgs)

    def test_install_provide(self):
        """Test that the pkg to be installed can be specified by provide."""
        self.base.install("henry(the_horse)")
        self.assertGreater(self.base._goal.req_length(), 0)

    def test_install_reponame(self):
        """Test whether packages are filtered by the reponame."""
        result = itertools.chain(
            self.base.sack.query().installed(),
            dnf.subject.Subject('lotus-3-16').get_best_query(self.base.sack))

        self.base.install('lotus', reponame='main')
        self.assertResult(self.base, result)

        assert dnf.subject.Subject('lotus-3-17').get_best_query(self.base.sack), \
            ('the base must contain packages a package in another repo '
             'which matches the pattern but is preferred, otherwise the '
             'test makes no sense')

    def test_install_upgrade(self):
        """Test that the pkg to be installed can be an upgrade."""
        self.base.install('hole-1-2')
        installed, removed = self.installed_removed(self.base)
        self.assertGreaterEqual(
            installed,
            set(self.base.sack.query().available()._nevra('hole-1-2.x86_64')))
        self.assertGreaterEqual(
            removed,
            set(self.base.sack.query().installed().filter(name='hole')))


class MultilibBestTest(tests.support.ResultTestCase):

    """Tests for multilib_policy='best'.

    The test fixture consists of a dnf.Base instance that:

    - has conf.multilib_policy == "best"
    - has conf.best == False
    - contains a package "pepper-20-2" (The package cannot be installed.)
    - contains a package "lotus-3-16.x86_64" that contains "/usr/lib*/liblot*"
      in a "main" repository (The package can be installed.)
    - contains a package "lotus-3-16.i686" that contains "/usr/lib*/liblot*"
      in the "main" repository (The package can be installed.)
    - contains nothing that matches "/not/exist/"
    - contains a package "librita-1-1.x86_64" (The package is already
      installed.)
    - contains a package "hole-1-2.x86_64" (The package can be installed as an
      upgrade.)
    - contains a package "lotus-3-17.x86_64" not in the "main" repository (The
      package can be installed.)
    - contains a package "pepper-20-0.src"
    - contains a package "pepper-20-0.x86_64" (The package is already
      installed.)
    - contains nothing that matches "not-available"
    - contains a package "trampoline" that provides "henry(the_horse)" (The
      package can be installed.)
    - contains a package "lotus-3-17.i686" not in the "main" repository (The
      package can be installed.)
    - contains a package "hole-1-1.x86_64" (The package is already installed
      and is not available.)

    """

    REPOS = ['main', 'third_party', 'broken_deps']

    def setUp(self):
        super(MultilibBestTest, self).setUp()
        self.installed = self.base.sack.query().installed().run()
        self.assertEqual(self.base.conf.multilib_policy, "best")
        assert self.base.conf.best is False

    def test_install_conflict(self):
        """Test that the exception is raised if the package conflicts."""
        self.base.install('pepper-20-2')
        with self.assertRaises(dnf.exceptions.DepsolveError):
            self.base.resolve()

    def test_install_filename(self):
        """Test that the pkg to be installed can be specified by filename."""
        self.base.install("/usr/lib*/liblot*")
        inst, _ = self.installed_removed(self.base)
        self.assertCountEqual(map(str, inst), ['lotus-3-16.x86_64'])

        self.assertRaises(dnf.exceptions.MarkingError,
                          self.base.install, "/not/exist/")

    def test_install_installed(self):
        """Test that nothing changes if an installed package matches."""
        stdout = dnf.pycomp.StringIO()
        with tests.support.wiretap_logs('dnf', logging.INFO, stdout):
            self.base.install('librita')
        installed, removed = self.installed_removed(self.base)
        self.assertEmpty(installed)
        self.assertEmpty(removed)
        self.assertIn(
            'Package librita-1-1.x86_64 is already installed.',
            stdout.getvalue())

    def test_install_installonly(self):
        """Test that installonly packages are installed, not upgraded."""
        self.base.conf.installonlypkgs += ['hole']
        self.base.install('hole-1-2')
        installed, removed = self.installed_removed(self.base)
        self.assertGreaterEqual(
            installed,
            set(self.base.sack.query().available()._nevra('hole-1-2.x86_64')))
        self.assertEmpty(removed)

    def test_install_multilib(self):
        """Test that a pkg for only one architecture are installed."""
        cnt = self.base.install("lotus")
        self.assertEqual(cnt, 1)

        new_package, = dnf.subject.Subject('lotus-3-17.x86_64').get_best_query(self.base.sack)
        new_set = self.installed + [new_package]
        self.assertResult(self.base, new_set)

    def test_install_name_choice(self):
        """Test that the matching pkg that can be installed is installed."""
        # Don't install the SRPM.
        self.base.sack.add_excludes(
            dnf.subject.Subject('pepper.src').get_best_query(self.base.sack))

        stdout = dnf.pycomp.StringIO()
        with tests.support.wiretap_logs('dnf', logging.INFO, stdout):
            self.base.install('pepper')
        installed, removed = self.installed_removed(self.base)
        self.assertEmpty(installed | removed)
        self.assertIn(
            'Package pepper-20-0.x86_64 is already installed.',
            stdout.getvalue())

    def test_install_nonexistent(self):
        """Test that the exception is raised if no package matches."""
        with self.assertRaises(dnf.exceptions.MarkingError) as context:
            self.base.install('not-available')
        self.assertEqual(context.exception.pkg_spec, 'not-available')
        installed_pkgs = self.base.sack.query().installed().run()
        self.assertResult(self.base, installed_pkgs)

    def test_install_provide(self):
        """Test that the pkg to be installed can be specified by provide."""
        self.base.install("henry(the_horse)")
        self.assertGreater(self.base._goal.req_length(), 0)
        trampoline = self.base.sack.query().available().filter(
            name="trampoline")
        new_set = self.installed + trampoline.run()
        self.assertResult(self.base, new_set)

    def test_install_reponame(self):
        """Test whether packages are filtered by the reponame."""
        result = itertools.chain(
            self.base.sack.query().installed(),
            dnf.subject.Subject('lotus-3-16.x86_64')
            .get_best_query(self.base.sack))

        self.base.install('lotus', reponame='main')
        self.assertResult(self.base, result)

        assert dnf.subject.Subject('lotus-3-17.x86_64').get_best_query(self.base.sack), \
            ('the base must contain packages a package in another repo '
             'which matches the pattern but is preferred, otherwise the '
             'test makes no sense')

    def test_install_unavailable(self):
        """Test that nothing changes if an unavailable package matches."""
        stdout = dnf.pycomp.StringIO()
        with tests.support.wiretap_logs('dnf', logging.INFO, stdout):
            cnt = self.base.install('hole')
        self.assertEqual(cnt, 1)
        installed_pkgs = self.base.sack.query().installed().run()
        self.assertResult(self.base, installed_pkgs)
        self.assertIn(
            'Package hole-1-1.x86_64 is already installed.',
            stdout.getvalue())

    def test_install_upgrade(self):
        """Test that the pkg to be installed can be an upgrade."""
        self.base.install('hole-1-2')
        installed, removed = self.installed_removed(self.base)
        self.assertGreaterEqual(
            installed,
            set(self.base.sack.query().available()._nevra('hole-1-2.x86_64')))
        self.assertGreaterEqual(
            removed,
            set(self.base.sack.query().installed().filter(name='hole')))


class BestTrueTest(tests.support.ResultTestCase):

    """Tests for best=True.

    The test fixture consists of a dnf.Base instance that:

    - has conf.best == True
    - contains a package "pepper-20-2" (The package cannot be installed.)

    """

    REPOS = ['broken_deps']

    def setUp(self):
        super(BestTrueTest, self).setUp()
        self.base.conf.best = True

    def test_install_name_choice(self):
        """Test that the latest version of the matching pkg is installed."""
        with tests.support.mock.patch('logging.Logger.warning'):
            self.base.install('pepper')
        with self.assertRaises(dnf.exceptions.DepsolveError):
            self.base.resolve()
