# SPDX-License-Identifier: Apache-2.0
# Copyright Contributors to the Rez Project


"""
test dependency resolving algorithm
"""
import rez.exceptions
from rez.version import Requirement
from rez.solver import Solver, Cycle, SolverStatus
from rez.config import config
import unittest
from rez.tests.util import TestBase
import itertools


solver_verbosity = 1


class TestSolver(TestBase):
    @classmethod
    def setUpClass(cls):
        packages_path = cls.data_path("solver", "packages")
        cls.packages_path = [packages_path]
        cls.settings = dict(
            packages_path=cls.packages_path,
            package_filter=None)

    def _create_solvers(self, reqs):
        s1 = Solver(reqs,
                    self.packages_path,
                    optimised=True,
                    verbosity=solver_verbosity)
        s2 = Solver(reqs,
                    self.packages_path,
                    optimised=False,
                    verbosity=solver_verbosity)

        s_perms = []
        perms = itertools.permutations(reqs)
        for reqs_ in perms:
            s = Solver(reqs_,
                       self.packages_path,
                       optimised=True,
                       verbosity=solver_verbosity)
            s_perms.append(s)

        return (s1, s2, s_perms)

    def _solve(self, packages, expected_resolve):
        print()
        reqs = [Requirement(x) for x in packages]
        s1, s2, s_perms = self._create_solvers(reqs)

        s1.solve()
        self.assertEqual(s1.status, SolverStatus.solved)

        # ephemeral order doesn't matter, hence the sort
        resolve = (
            [str(x) for x in s1.resolved_packages]
            + sorted(str(x) for x in s1.resolved_ephemerals)
        )

        print()
        print("request: %s" % ' '.join(packages))
        print("expecting: %s" % ' '.join(expected_resolve))
        print("result: %s" % ' '.join(str(x) for x in resolve))
        self.assertEqual(resolve, expected_resolve)

        print("checking that unoptimised solve matches optimised...")
        s2.solve()
        self.assertEqual(s2.status, SolverStatus.solved)
        resolve2 = (
            [str(x) for x in s2.resolved_packages]
            + sorted(str(x) for x in s2.resolved_ephemerals)
        )
        self.assertEqual(resolve2, resolve)

        print("checking that permutations also succeed...")
        for s in s_perms:
            s.solve()
            self.assertEqual(s.status, SolverStatus.solved)

        return s1

    def _fail(self, *packages):
        print()
        reqs = [Requirement(x) for x in packages]
        s1, s2, s_perms = self._create_solvers(reqs)

        s1.solve()
        print()
        print("request: %s" % ' '.join(packages))
        print("expecting failure")
        self.assertEqual(s1.status, SolverStatus.failed)
        print("result: %s" % str(s1.failure_reason()))

        print("checking that unoptimised solve fail matches optimised...")
        s2.solve()
        self.assertEqual(s2.status, SolverStatus.failed)
        self.assertEqual(s1.failure_reason(), s2.failure_reason())

        print("checking that permutations also fail...")
        for s in s_perms:
            s.solve()
            self.assertEqual(s.status, SolverStatus.failed)

        return s1

    def test_01(self):
        """Extremely basic solves involving a single package."""
        self._solve([],
                    [])
        self._solve(["nada"],
                    ["nada[]"])
        self._solve(["!nada"],
                    [])
        self._solve(["~nada"],
                    [])
        self._solve([".foo-1"],
                    [".foo-1"])
        self._solve(["!.bah"],
                    [])
        self._solve(["!.bah-2.5+"],
                    [])
        self._solve(["python"],
                    ["python-2.7.0[]"])
        self._solve(["~python-2+"],
                    [])
        self._solve(["~python"],
                    [])
        self._solve(["!python-2.5"],
                    [])
        self._solve(["!python"],
                    [])

    def test_02(self):
        """Basic solves involving a single package."""
        self._solve(["nada", "~nada"],
                    ["nada[]"])
        self._solve(["nopy"],
                    ["nopy-2.1[]"])
        self._solve(["python-2.6"],
                    ["python-2.6.8[]"])
        self._solve(["python-2.6", "!python-2.6.8"],
                    ["python-2.6.0[]"])
        self._solve(["python-2.6", "python-2.6.5+"],
                    ["python-2.6.8[]"])
        self._solve(["python", "python-0+<2.6"],
                    ["python-2.5.2[]"])
        self._solve(["python", "python-0+<2.6.8"],
                    ["python-2.6.0[]"])
        self._solve(["python", "~python-2.7+"],
                    ["python-2.7.0[]"])
        self._solve(["!python-2.6+", "python"],
                    ["python-2.5.2[]"])

    def test_03(self):
        """Failures in the initial request."""
        self._fail("nada", "!nada")
        self._fail("python-2.6", "~python-2.7")
        self._fail("pyfoo", "nada", "!nada")
        self._fail(".foo-1", ".foo-2")
        self._fail(".foo-2.5", "!.foo-2")

    def test_04(self):
        """Basic failures."""
        self._fail("pybah", "!python")
        self._fail("pyfoo-3.1", "python-2.7+")
        self._fail("pyodd<2", "python-2.7")
        self._fail("nopy", "python-2.5.2")

    def test_05(self):
        """More complex failures."""
        self._fail("bahish", "pybah<5")
        self._fail("pybah-4", "pyfoo-3.0")

    def test_06(self):
        """Basic solves involving multiple packages."""
        self._solve(["nada", "nopy"],
                    ["nada[]", "nopy-2.1[]"])
        self._solve(["pyfoo"],
                    ["python-2.6.8[]", "pyfoo-3.1.0[]"])
        self._solve(["pyfoo-3.0"],
                    ["python-2.5.2[]", "pyfoo-3.0.0[]", ".eek-3+"])
        self._solve(["pyfoo-3.0", ".eek-4.5"],
                    ["python-2.5.2[]", "pyfoo-3.0.0[]", ".eek-4.5"])
        self._solve(["pybah"],
                    ["python-2.5.2[]", "pybah-5[]"])
        self._solve(["nopy", "python"],
                    ["nopy-2.1[]", "python-2.7.0[]"])
        self._solve(["pybah", "!python-2.5"],
                    ["python-2.6.8[]", "pybah-4[]", ".eek-1"])
        self._solve(["pybah", "!python-2.5", "python<2.6.8"],
                    ["python-2.6.0[]", "pybah-4[]", ".eek-1"])
        self._solve(["python", "pybah"],
                    ["python-2.6.8[]", "pybah-4[]", ".eek-1"])

    def test_07(self):
        """More complex solves."""
        self._solve(["python", "pyodd"],
                    ["python-2.6.8[]", "pybah-4[]", "pyodd-2[]", ".eek-1"])
        self._solve(["pybah", "pyodd"],
                    ["python-2.5.2[]", "pybah-5[]", "pyodd-2[]"])
        self._solve(["pysplit", "python-2.5"],
                    ["pysplit-5[]", "python-2.5.2[]"])
        self._solve(["~python<2.6", "pysplit"],
                    ["pysplit-5[]"])
        self._solve(["python", "bahish", "pybah"],
                    ["python-2.5.2[]", "pybah-5[]", "bahish-2[]"])
        self._solve([".foo-2.5+", ".foo-2"],
                    [".foo-2.5+<2_"])

    def test_08(self):
        """Cyclic failures."""

        def _test(*pkgs):
            s = self._fail(*pkgs)
            self.assertTrue(isinstance(s.failure_reason(), Cycle))

        _test("pymum-1")
        _test("pydad-1")
        _test("pyson-1")
        _test("pymum-3")
        _test("pydad-3")
        s = self._fail("pymum-2")
        self.assertFalse(isinstance(s.failure_reason(), Cycle))

    # variant tests

    def test_09_version_priority_mode(self):
        config.override("variant_select_mode", "version_priority")
        self._solve(["pyvariants", "python"],
                    ["python-2.7.0[]", "pyvariants-2[0]"])
        self._solve(["pyvariants", "python", "nada"],
                    ["python-2.7.0[]", "pyvariants-2[0]", "nada[]"])

    def test_10_intersection_priority_mode(self):
        config.override("variant_select_mode", "intersection_priority")
        self._solve(["pyvariants", "python"],
                    ["python-2.7.0[]", "pyvariants-2[0]"])
        self._solve(["pyvariants", "python", "nada"],
                    ["python-2.6.8[]", "nada[]", "pyvariants-2[1]"])

    def test_11_variant_splitting(self):
        self._solve(["test_variant_split_start"],
                    ["test_variant_split_end-1.0[1]",
                     "test_variant_split_mid2-2.0[0]",
                     "test_variant_split_start-1.0[1]"])

    def test_12_missing_variant_requires(self):
        config.override("error_on_missing_variant_requires", True)
        with self.assertRaises(rez.exceptions.PackageFamilyNotFoundError):
            self._solve(["missing_variant_requires"], [])

        config.override("error_on_missing_variant_requires", False)
        self._solve(["missing_variant_requires"], ["nada[]", "missing_variant_requires-1[1]"])

    def test_13_resolve_weakly_reference_requires(self):
        """Test resolving a package with a weakly referenced requirement."""
        self._solve(["test_weakly_reference_requires", "test_variant_split_mid2-2"],
                    ['test_weakly_reference_requires-2.0[]',
                     'test_variant_split_end-3.0[0]',
                     'test_variant_split_mid2-2.0[1]'])

    def test_14_resolve_weakly_reference_variant(self):
        """Test resolving a package with a weakly referenced variant."""
        self._solve(["test_weakly_reference_variant-2.0", "test_variant_split_mid2-2", "pyfoo"],
                    ['test_variant_split_end-1.0[1]',
                     'test_variant_split_mid1-1.0[1]',
                     'test_weakly_reference_variant-2.0[0]',
                     'test_variant_split_mid2-2.0[0]',
                     'python-2.6.8[]',
                     'pyfoo-3.1.0[]'])


if __name__ == '__main__':
    unittest.main()
