# Copyright (C) 2012 Anaconda, Inc
# SPDX-License-Identifier: BSD-3-Clause
"""Helpers for testing the solver."""

from __future__ import annotations

import collections
import functools
import json
import pathlib
import time
from tempfile import TemporaryDirectory

import pytest

from ..base.context import context
from ..core.solve import Solver
from ..exceptions import (
    PackagesNotFoundError,
    ResolvePackageNotFound,
    UnsatisfiableError,
)
from ..models.channel import Channel
from ..models.match_spec import MatchSpec
from ..models.records import PackageRecord
from . import helpers


@functools.cache
def index_packages(num):
    """Get the index data of the ``helpers.get_index_r_*`` helpers."""
    # XXX: get_index_r_X should probably be refactored to avoid loading the environment like this.
    get_index = getattr(helpers, f"get_index_r_{num}")
    index, _ = get_index(context.subdir)
    return list(index.values())


def package_string(record):
    return f"{record.channel.name}::{record.name}-{record.version}-{record.build}"


def package_string_set(packages):
    """Transforms package container in package string set."""
    return {package_string(record) for record in packages}


def package_dict(packages):
    """Transforms package container into a dictionary."""
    return {record.name: record for record in packages}


class SimpleEnvironment:
    """Helper environment object."""

    REPO_DATA_KEYS = (
        "build",
        "build_number",
        "depends",
        "license",
        "md5",
        "name",
        "sha256",
        "size",
        "subdir",
        "timestamp",
        "version",
        "track_features",
        "features",
    )

    def __init__(self, path, solver_class, subdirs=context.subdirs):
        self._path = pathlib.Path(path)
        self._prefix_path = self._path / "prefix"
        self._channels_path = self._path / "channels"
        self._solver_class = solver_class
        self.subdirs = subdirs
        self.installed_packages = []
        # if repo_packages is a list, the packages will be put in a `test` channel
        # if it is a dictionary, it the keys are the channel name and the value
        # the channel packages
        self.repo_packages: list[str] | dict[str, list[str]] = []

    def solver(self, add, remove):
        """Writes ``repo_packages`` to the disk and creates a solver instance."""
        channels = []
        self._write_installed_packages()
        for channel_name, packages in self._channel_packages.items():
            self._write_repo_packages(channel_name, packages)
            channel = Channel(str(self._channels_path / channel_name))
            channels.append(channel)
        return self._solver_class(
            prefix=self._prefix_path,
            subdirs=self.subdirs,
            channels=channels,
            specs_to_add=add,
            specs_to_remove=remove,
        )

    def solver_transaction(self, add=(), remove=(), as_specs=False):
        packages = self.solver(add=add, remove=remove).solve_final_state()
        if as_specs:
            return packages
        return package_string_set(packages)

    def install(self, *specs, as_specs=False):
        return self.solver_transaction(add=specs, as_specs=as_specs)

    def remove(self, *specs, as_specs=False):
        return self.solver_transaction(remove=specs, as_specs=as_specs)

    @property
    def _channel_packages(self):
        """Helper that unfolds the ``repo_packages`` into a dictionary."""
        if isinstance(self.repo_packages, dict):
            return self.repo_packages
        return {"test": self.repo_packages}

    def _package_data(self, record):
        """Turn record into data, to be written in the JSON environment/repo files."""
        data = {
            key: value
            for key, value in vars(record).items()
            if key in self.REPO_DATA_KEYS
        }
        if "subdir" not in data:
            data["subdir"] = context.subdir
        return data

    def _write_installed_packages(self):
        if not self.installed_packages:
            return
        conda_meta = self._prefix_path / "conda-meta"
        conda_meta.mkdir(exist_ok=True, parents=True)
        # write record files
        for record in self.installed_packages:
            record_path = (
                conda_meta / f"{record.name}-{record.version}-{record.build}.json"
            )
            record_data = self._package_data(record)
            record_data["channel"] = record.channel.name
            record_path.write_text(json.dumps(record_data))
        # write history file
        history_path = conda_meta / "history"
        history_path.write_text(
            "\n".join(
                (
                    "==> 2000-01-01 00:00:00 <==",
                    *map(package_string, self.installed_packages),
                )
            )
        )

    def _write_repo_packages(self, channel_name, packages):
        """Write packages to the channel path."""
        # build package data
        package_data = collections.defaultdict(dict)
        for record in packages:
            package_data[record.subdir][record.fn] = self._package_data(record)
        # write repodata
        assert set(self.subdirs).issuperset(set(package_data.keys()))
        for subdir in self.subdirs:
            subdir_path = self._channels_path / channel_name / subdir
            subdir_path.mkdir(parents=True, exist_ok=True)
            subdir_path.joinpath("repodata.json").write_text(
                json.dumps(
                    {
                        "info": {
                            "subdir": subdir,
                        },
                        "packages": package_data.get(subdir, {}),
                    }
                )
            )


def empty_prefix():
    return TemporaryDirectory(prefix="conda-test-repo-")


@pytest.fixture()
def temp_simple_env(solver_class=Solver) -> SimpleEnvironment:
    with empty_prefix() as prefix:
        yield SimpleEnvironment(prefix, solver_class)


class SolverTests:
    """Tests for :py:class:`conda.core.solve.Solver` implementations."""

    @property
    def solver_class(self) -> type[Solver]:
        """Class under test."""
        raise NotImplementedError

    @property
    def tests_to_skip(self):
        return {}  # skip reason -> list of tests to skip

    @pytest.fixture(autouse=True)
    def skip_tests(self, request):
        for reason, skip_list in self.tests_to_skip.items():
            if request.node.name in skip_list:
                pytest.skip(reason)

    @pytest.fixture()
    def env(self):
        with TemporaryDirectory(prefix="conda-test-repo-") as tmpdir:
            self.env = SimpleEnvironment(tmpdir, self.solver_class)
            yield self.env
            self.env = None

    def find_package_in_list(self, packages, **kwargs):
        for record in packages:
            if all(getattr(record, key) == value for key, value in kwargs.items()):
                return record

    def find_package(self, **kwargs):
        if isinstance(self.env.repo_packages, dict):
            if "channel" not in kwargs:
                raise ValueError(
                    "Repo has multiple channels, the `channel` argument must be specified"
                )
            packages = self.env.repo_packages[kwargs["channel"]]
        else:
            packages = self.env.repo_packages
        return self.find_package_in_list(packages, **kwargs)

    def assert_unsatisfiable(self, exc_info, entries):
        """Helper to assert that a :py:class:`conda.exceptions.UnsatisfiableError`
        instance as a the specified set of unsatisfiable specifications.
        """
        assert issubclass(exc_info.type, UnsatisfiableError)
        if exc_info.type is UnsatisfiableError:
            assert (
                sorted(
                    tuple(map(str, entries)) for entries in exc_info.value.unsatisfiable
                )
                == entries
            )

    def test_empty(self, env):
        env.repo_packages = index_packages(1)
        assert env.install() == set()

    def test_iopro_mkl(self, env):
        env.repo_packages = index_packages(1)
        assert env.install("iopro 1.4*", "python 2.7*", "numpy 1.7*") == {
            "test::iopro-1.4.3-np17py27_p0",
            "test::numpy-1.7.1-py27_0",
            "test::openssl-1.0.1c-0",
            "test::python-2.7.5-0",
            "test::readline-6.2-0",
            "test::sqlite-3.7.13-0",
            "test::system-5.8-1",
            "test::tk-8.5.13-0",
            "test::unixodbc-2.3.1-0",
            "test::zlib-1.2.7-0",
            "test::distribute-0.6.36-py27_1",
            "test::pip-1.3.1-py27_1",
        }

    def test_iopro_nomkl(self, env):
        env.repo_packages = index_packages(1)
        assert env.install(
            "iopro 1.4*", "python 2.7*", "numpy 1.7*", MatchSpec(track_features="mkl")
        ) == {
            "test::iopro-1.4.3-np17py27_p0",
            "test::mkl-rt-11.0-p0",
            "test::numpy-1.7.1-py27_p0",
            "test::openssl-1.0.1c-0",
            "test::python-2.7.5-0",
            "test::readline-6.2-0",
            "test::sqlite-3.7.13-0",
            "test::system-5.8-1",
            "test::tk-8.5.13-0",
            "test::unixodbc-2.3.1-0",
            "test::zlib-1.2.7-0",
            "test::distribute-0.6.36-py27_1",
            "test::pip-1.3.1-py27_1",
        }

    def test_mkl(self, env):
        env.repo_packages = index_packages(1)
        assert env.install("mkl") == env.install(
            "mkl 11*", MatchSpec(track_features="mkl")
        )

    def test_accelerate(self, env):
        env.repo_packages = index_packages(1)
        assert env.install("accelerate") == env.install(
            "accelerate", MatchSpec(track_features="mkl")
        )

    def test_scipy_mkl(self, env):
        env.repo_packages = index_packages(1)
        records = env.install(
            "scipy",
            "python 2.7*",
            "numpy 1.7*",
            MatchSpec(track_features="mkl"),
            as_specs=True,
        )

        for record in records:
            if record.name in ("numpy", "scipy"):
                assert "mkl" in record.features

        assert "test::numpy-1.7.1-py27_p0" in package_string_set(records)
        assert "test::scipy-0.12.0-np17py27_p0" in package_string_set(records)

    def test_anaconda_nomkl(self, env):
        env.repo_packages = index_packages(1)
        records = env.install("anaconda 1.5.0", "python 2.7*", "numpy 1.7*")
        assert len(records) == 107
        assert "test::scipy-0.12.0-np17py27_0" in records

    def test_pseudo_boolean(self, env):
        env.repo_packages = index_packages(1)
        # The latest version of iopro, 1.5.0, was not built against numpy 1.5
        assert env.install("iopro", "python 2.7*", "numpy 1.5*") == {
            "test::iopro-1.4.3-np15py27_p0",
            "test::numpy-1.5.1-py27_4",
            "test::openssl-1.0.1c-0",
            "test::python-2.7.5-0",
            "test::readline-6.2-0",
            "test::sqlite-3.7.13-0",
            "test::system-5.8-1",
            "test::tk-8.5.13-0",
            "test::unixodbc-2.3.1-0",
            "test::zlib-1.2.7-0",
            "test::distribute-0.6.36-py27_1",
            "test::pip-1.3.1-py27_1",
        }
        assert env.install(
            "iopro", "python 2.7*", "numpy 1.5*", MatchSpec(track_features="mkl")
        ) == {
            "test::iopro-1.4.3-np15py27_p0",
            "test::mkl-rt-11.0-p0",
            "test::numpy-1.5.1-py27_p4",
            "test::openssl-1.0.1c-0",
            "test::python-2.7.5-0",
            "test::readline-6.2-0",
            "test::sqlite-3.7.13-0",
            "test::system-5.8-1",
            "test::tk-8.5.13-0",
            "test::unixodbc-2.3.1-0",
            "test::zlib-1.2.7-0",
            "test::distribute-0.6.36-py27_1",
            "test::pip-1.3.1-py27_1",
        }

    def test_unsat_from_r1(self, env):
        env.repo_packages = index_packages(1)

        with pytest.raises(UnsatisfiableError) as exc_info:
            env.install("numpy 1.5*", "scipy 0.12.0b1")
        self.assert_unsatisfiable(
            exc_info,
            [
                ("numpy=1.5",),
                ("scipy==0.12.0b1", "numpy[version='1.6.*|1.7.*']"),
            ],
        )

        with pytest.raises(UnsatisfiableError) as exc_info:
            env.install("numpy 1.5*", "python 3*")
        self.assert_unsatisfiable(
            exc_info,
            [
                ("numpy=1.5", "nose", "python=3.3"),
                ("numpy=1.5", "python[version='2.6.*|2.7.*']"),
                ("python=3",),
            ],
        )

        with pytest.raises((ResolvePackageNotFound, PackagesNotFoundError)) as exc_info:
            env.install("numpy 1.5*", "numpy 1.6*")
        if exc_info.type is ResolvePackageNotFound:
            assert sorted(map(str, exc_info.value.bad_deps)) == [
                "numpy[version='1.5.*,1.6.*']",
            ]

    def test_unsat_simple(self, env):
        env.repo_packages = [
            helpers.record(name="a", depends=["c >=1,<2"]),
            helpers.record(name="b", depends=["c >=2,<3"]),
            helpers.record(name="c", version="1.0"),
            helpers.record(name="c", version="2.0"),
        ]
        with pytest.raises(UnsatisfiableError) as exc_info:
            env.install("a", "b")
        self.assert_unsatisfiable(
            exc_info,
            [
                ("a", "c[version='>=1,<2']"),
                ("b", "c[version='>=2,<3']"),
            ],
        )

    def test_get_dists(self, env):
        env.repo_packages = index_packages(1)
        records = env.install("anaconda 1.4.0")
        assert "test::anaconda-1.4.0-np17py33_0" in records
        assert "test::freetype-2.4.10-0" in records

    def test_unsat_shortest_chain_1(self, env):
        env.repo_packages = [
            helpers.record(name="a", depends=["d", "c <1.3.0"]),
            helpers.record(name="b", depends=["c"]),
            helpers.record(
                name="c",
                version="1.3.6",
            ),
            helpers.record(
                name="c",
                version="1.2.8",
            ),
            helpers.record(name="d", depends=["c >=0.8.0"]),
        ]
        with pytest.raises(UnsatisfiableError) as exc_info:
            env.install("c=1.3.6", "a", "b")
        self.assert_unsatisfiable(
            exc_info,
            [
                ("a", "c[version='<1.3.0']"),
                ("a", "d", "c[version='>=0.8.0']"),
                ("b", "c"),
                ("c=1.3.6",),
            ],
        )

    def test_unsat_shortest_chain_2(self, env):
        env.repo_packages = [
            helpers.record(name="a", depends=["d", "c >=0.8.0"]),
            helpers.record(name="b", depends=["c"]),
            helpers.record(
                name="c",
                version="1.3.6",
            ),
            helpers.record(
                name="c",
                version="1.2.8",
            ),
            helpers.record(name="d", depends=["c <1.3.0"]),
        ]
        with pytest.raises(UnsatisfiableError) as exc_info:
            env.install("c=1.3.6", "a", "b")
        self.assert_unsatisfiable(
            exc_info,
            [
                ("a", "c[version='>=0.8.0']"),
                ("a", "d", "c[version='<1.3.0']"),
                ("b", "c"),
                ("c=1.3.6",),
            ],
        )

    def test_unsat_shortest_chain_3(self, env):
        env.repo_packages = [
            helpers.record(name="a", depends=["f", "e"]),
            helpers.record(name="b", depends=["c"]),
            helpers.record(
                name="c",
                version="1.3.6",
            ),
            helpers.record(
                name="c",
                version="1.2.8",
            ),
            helpers.record(name="d", depends=["c >=0.8.0"]),
            helpers.record(name="e", depends=["c <1.3.0"]),
            helpers.record(name="f", depends=["d"]),
        ]
        with pytest.raises(UnsatisfiableError) as exc_info:
            env.install("c=1.3.6", "a", "b")
        self.assert_unsatisfiable(
            exc_info,
            [
                ("a", "e", "c[version='<1.3.0']"),
                ("b", "c"),
                ("c=1.3.6",),
            ],
        )

    def test_unsat_shortest_chain_4(self, env):
        env.repo_packages = [
            helpers.record(name="a", depends=["py =3.7.1"]),
            helpers.record(name="py_req_1"),
            helpers.record(name="py_req_2"),
            helpers.record(
                name="py", version="3.7.1", depends=["py_req_1", "py_req_2"]
            ),
            helpers.record(
                name="py", version="3.6.1", depends=["py_req_1", "py_req_2"]
            ),
        ]
        with pytest.raises(UnsatisfiableError) as exc_info:
            env.install("a", "py=3.6.1")
        self.assert_unsatisfiable(
            exc_info,
            [
                ("a", "py=3.7.1"),
                ("py=3.6.1",),
            ],
        )

    def test_unsat_chain(self, env):
        # a -> b -> c=1.x -> d=1.x
        # e      -> c=2.x -> d=2.x
        env.repo_packages = [
            helpers.record(name="a", depends=["b"]),
            helpers.record(name="b", depends=["c >=1,<2"]),
            helpers.record(name="c", version="1.0", depends=["d >=1,<2"]),
            helpers.record(name="d", version="1.0"),
            helpers.record(name="e", depends=["c >=2,<3"]),
            helpers.record(name="c", version="2.0", depends=["d >=2,<3"]),
            helpers.record(name="d", version="2.0"),
        ]
        with pytest.raises(UnsatisfiableError) as exc_info:
            env.install("a", "e")
        self.assert_unsatisfiable(
            exc_info,
            [
                ("a", "b", "c[version='>=1,<2']"),
                ("e", "c[version='>=2,<3']"),
            ],
        )

    def test_unsat_any_two_not_three(self, env):
        # can install any two of a, b and c but not all three
        env.repo_packages = [
            helpers.record(name="a", version="1.0", depends=["d >=1,<2"]),
            helpers.record(name="a", version="2.0", depends=["d >=2,<3"]),
            helpers.record(name="b", version="1.0", depends=["d >=1,<2"]),
            helpers.record(name="b", version="2.0", depends=["d >=3,<4"]),
            helpers.record(name="c", version="1.0", depends=["d >=2,<3"]),
            helpers.record(name="c", version="2.0", depends=["d >=3,<4"]),
            helpers.record(name="d", version="1.0"),
            helpers.record(name="d", version="2.0"),
            helpers.record(name="d", version="3.0"),
        ]
        # a and b can be installed
        installed = env.install("a", "b", as_specs=True)
        assert any(k.name == "a" and k.version == "1.0" for k in installed)
        assert any(k.name == "b" and k.version == "1.0" for k in installed)
        # a and c can be installed
        installed = env.install("a", "c", as_specs=True)
        assert any(k.name == "a" and k.version == "2.0" for k in installed)
        assert any(k.name == "c" and k.version == "1.0" for k in installed)
        # b and c can be installed
        installed = env.install("b", "c", as_specs=True)
        assert any(k.name == "b" and k.version == "2.0" for k in installed)
        assert any(k.name == "c" and k.version == "2.0" for k in installed)
        # a, b and c cannot be installed
        with pytest.raises(UnsatisfiableError) as exc_info:
            env.install("a", "b", "c")
        self.assert_unsatisfiable(
            exc_info,
            [
                ("a", "d[version='>=1,<2|>=2,<3']"),
                ("b", "d[version='>=1,<2|>=3,<4']"),
                ("c", "d[version='>=2,<3|>=3,<4']"),
            ],
        )

    def test_unsat_expand_single(self, env):
        env.repo_packages = [
            helpers.record(name="a", depends=["b", "c"]),
            helpers.record(name="b", depends=["d >=1,<2"]),
            helpers.record(name="c", depends=["d >=2,<3"]),
            helpers.record(name="d", version="1.0"),
            helpers.record(name="d", version="2.0"),
        ]
        with pytest.raises(UnsatisfiableError) as exc_info:
            env.install("a")
        self.assert_unsatisfiable(
            exc_info,
            [
                ("b", "d[version='>=1,<2']"),
                ("c", "d[version='>=2,<3']"),
            ],
        )

    def test_unsat_missing_dep(self, env):
        env.repo_packages = [
            helpers.record(name="a", depends=["b", "c"]),
            helpers.record(name="b", depends=["c >=2,<3"]),
            helpers.record(name="c", version="1.0"),
        ]
        with pytest.raises(UnsatisfiableError) as exc_info:
            env.install("a", "b")
        self.assert_unsatisfiable(
            exc_info,
            [
                ("a", "b"),
                ("b",),
            ],
        )

    def test_nonexistent(self, env):
        with pytest.raises((ResolvePackageNotFound, PackagesNotFoundError)):
            env.install("notarealpackage 2.0*")
        with pytest.raises((ResolvePackageNotFound, PackagesNotFoundError)):
            env.install("numpy 1.5")

    def test_timestamps_and_deps(self, env):
        env.repo_packages = index_packages(1) + [
            helpers.record(
                name="mypackage",
                version="1.0",
                build="hash12_0",
                timestamp=1,
                depends=["libpng 1.2.*"],
            ),
            helpers.record(
                name="mypackage",
                version="1.0",
                build="hash15_0",
                timestamp=0,
                depends=["libpng 1.5.*"],
            ),
        ]
        # libpng 1.2
        records_12 = env.install("libpng 1.2.*", "mypackage")
        assert "test::libpng-1.2.50-0" in records_12
        assert "test::mypackage-1.0-hash12_0" in records_12
        # libpng 1.5
        records_15 = env.install("libpng 1.5.*", "mypackage")
        assert "test::libpng-1.5.13-1" in records_15
        assert "test::mypackage-1.0-hash15_0" in records_15
        # this is testing that previously installed reqs are not disrupted
        # by newer timestamps. regression test of sorts for
        #  https://github.com/conda/conda/issues/6271
        assert (
            env.install("mypackage", *env.install("libpng 1.2.*", as_specs=True))
            == records_12
        )
        assert (
            env.install("mypackage", *env.install("libpng 1.5.*", as_specs=True))
            == records_15
        )
        # unspecified python version should maximize libpng (v1.5),
        # even though it has a lower timestamp
        assert env.install("mypackage") == records_15

    def test_nonexistent_deps(self, env):
        env.repo_packages = index_packages(1) + [
            helpers.record(
                name="mypackage",
                version="1.0",
                depends=["nose", "python 3.3*", "notarealpackage 2.0*"],
            ),
            helpers.record(
                name="mypackage",
                version="1.1",
                depends=["nose", "python 3.3*"],
            ),
            helpers.record(
                name="anotherpackage",
                version="1.0",
                depends=["nose", "mypackage 1.1"],
            ),
            helpers.record(
                name="anotherpackage",
                version="2.0",
                depends=["nose", "mypackage"],
            ),
        ]
        # XXX: missing find_matches and reduced_index
        assert env.install("mypackage") == {
            "test::mypackage-1.1-0",
            "test::nose-1.3.0-py33_0",
            "test::openssl-1.0.1c-0",
            "test::python-3.3.2-0",
            "test::readline-6.2-0",
            "test::sqlite-3.7.13-0",
            "test::system-5.8-1",
            "test::tk-8.5.13-0",
            "test::zlib-1.2.7-0",
            "test::distribute-0.6.36-py33_1",
            "test::pip-1.3.1-py33_1",
        }
        assert env.install("anotherpackage 1.0") == {
            "test::anotherpackage-1.0-0",
            "test::mypackage-1.1-0",
            "test::nose-1.3.0-py33_0",
            "test::openssl-1.0.1c-0",
            "test::python-3.3.2-0",
            "test::readline-6.2-0",
            "test::sqlite-3.7.13-0",
            "test::system-5.8-1",
            "test::tk-8.5.13-0",
            "test::zlib-1.2.7-0",
            "test::distribute-0.6.36-py33_1",
            "test::pip-1.3.1-py33_1",
        }
        assert env.install("anotherpackage") == {
            "test::anotherpackage-2.0-0",
            "test::mypackage-1.1-0",
            "test::nose-1.3.0-py33_0",
            "test::openssl-1.0.1c-0",
            "test::python-3.3.2-0",
            "test::readline-6.2-0",
            "test::sqlite-3.7.13-0",
            "test::system-5.8-1",
            "test::tk-8.5.13-0",
            "test::zlib-1.2.7-0",
            "test::distribute-0.6.36-py33_1",
            "test::pip-1.3.1-py33_1",
        }

        # Add 1s to make sure the new repodata.jsons have different mod times
        time.sleep(1)

        # This time, the latest version is messed up
        env.repo_packages = index_packages(1) + [
            helpers.record(
                name="mypackage",
                version="1.0",
                depends=["nose", "python 3.3*"],
            ),
            helpers.record(
                name="mypackage",
                version="1.1",
                depends=["nose", "python 3.3*", "notarealpackage 2.0*"],
            ),
            helpers.record(
                name="anotherpackage",
                version="1.0",
                depends=["nose", "mypackage 1.0"],
            ),
            helpers.record(
                name="anotherpackage",
                version="2.0",
                depends=["nose", "mypackage"],
            ),
        ]
        # XXX: missing find_matches and reduced_index
        assert env.install("mypackage") == {
            "test::mypackage-1.0-0",
            "test::nose-1.3.0-py33_0",
            "test::openssl-1.0.1c-0",
            "test::python-3.3.2-0",
            "test::readline-6.2-0",
            "test::sqlite-3.7.13-0",
            "test::system-5.8-1",
            "test::tk-8.5.13-0",
            "test::zlib-1.2.7-0",
            "test::distribute-0.6.36-py33_1",
            "test::pip-1.3.1-py33_1",
        }
        # TODO: We need UnsatisfiableError here because mamba does not
        # have more granular exceptions yet.
        with pytest.raises((ResolvePackageNotFound, UnsatisfiableError)):
            env.install("mypackage 1.1")
        assert env.install("anotherpackage 1.0") == {
            "test::anotherpackage-1.0-0",
            "test::mypackage-1.0-0",
            "test::nose-1.3.0-py33_0",
            "test::openssl-1.0.1c-0",
            "test::python-3.3.2-0",
            "test::readline-6.2-0",
            "test::sqlite-3.7.13-0",
            "test::system-5.8-1",
            "test::tk-8.5.13-0",
            "test::zlib-1.2.7-0",
            "test::distribute-0.6.36-py33_1",
            "test::pip-1.3.1-py33_1",
        }

        # If recursive checking is working correctly, this will give
        # anotherpackage 2.0, not anotherpackage 1.0
        assert env.install("anotherpackage") == {
            "test::anotherpackage-2.0-0",
            "test::mypackage-1.0-0",
            "test::nose-1.3.0-py33_0",
            "test::openssl-1.0.1c-0",
            "test::python-3.3.2-0",
            "test::readline-6.2-0",
            "test::sqlite-3.7.13-0",
            "test::system-5.8-1",
            "test::tk-8.5.13-0",
            "test::zlib-1.2.7-0",
            "test::distribute-0.6.36-py33_1",
            "test::pip-1.3.1-py33_1",
        }

    def test_install_package_with_feature(self, env):
        env.repo_packages = index_packages(1) + [
            helpers.record(
                name="mypackage",
                version="1.0",
                depends=["python 3.3*"],
                features="feature",
            ),
            helpers.record(
                name="feature",
                version="1.0",
                depends=["python 3.3*"],
                track_features="feature",
            ),
        ]
        # should not raise
        env.install("mypackage", "feature 1.0")

    def test_unintentional_feature_downgrade(self, env):
        # See https://github.com/conda/conda/issues/6765
        # With the bug in place, this bad build of scipy
        # will be selected for install instead of a later
        # build of scipy 0.11.0.
        good_rec_match = MatchSpec("channel-1::scipy==0.11.0=np17py33_3")
        good_rec = next(
            prec for prec in index_packages(1) if good_rec_match.match(prec)
        )
        bad_deps = tuple(d for d in good_rec.depends if not d.startswith("numpy"))
        bad_rec = PackageRecord.from_objects(
            good_rec,
            channel="test",
            build=good_rec.build.replace("_3", "_x0"),
            build_number=0,
            depends=bad_deps,
            fn=good_rec.fn.replace("_3", "_x0"),
            url=good_rec.url.replace("_3", "_x0"),
        )

        env.repo_packages = index_packages(1) + [bad_rec]
        records = env.install("scipy 0.11.0")
        assert "test::scipy-0.11.0-np17py33_x0" not in records
        assert "test::scipy-0.11.0-np17py33_3" in records

    def test_circular_dependencies(self, env):
        env.repo_packages = index_packages(1) + [
            helpers.record(
                name="package1",
                depends=["package2"],
            ),
            helpers.record(
                name="package2",
                depends=["package1"],
            ),
        ]
        assert (
            env.install("package1", "package2")
            == env.install("package1")
            == env.install("package2")
        )

    def test_irrational_version(self, env):
        env.repo_packages = index_packages(1)
        assert env.install("pytz 2012d", "python 3*") == {
            "test::distribute-0.6.36-py33_1",
            "test::openssl-1.0.1c-0",
            "test::pip-1.3.1-py33_1",
            "test::python-3.3.2-0",
            "test::pytz-2012d-py33_0",
            "test::readline-6.2-0",
            "test::sqlite-3.7.13-0",
            "test::system-5.8-1",
            "test::tk-8.5.13-0",
            "test::zlib-1.2.7-0",
        }

    def test_no_features(self, env):
        env.repo_packages = index_packages(1)

        assert env.install("python 2.6*", "numpy 1.6*", "scipy 0.11*") == {
            "test::distribute-0.6.36-py26_1",
            "test::numpy-1.6.2-py26_4",
            "test::openssl-1.0.1c-0",
            "test::pip-1.3.1-py26_1",
            "test::python-2.6.8-6",
            "test::readline-6.2-0",
            "test::scipy-0.11.0-np16py26_3",
            "test::sqlite-3.7.13-0",
            "test::system-5.8-1",
            "test::tk-8.5.13-0",
            "test::zlib-1.2.7-0",
        }
        assert env.install(
            "python 2.6*", "numpy 1.6*", "scipy 0.11*", MatchSpec(track_features="mkl")
        ) == {
            "test::distribute-0.6.36-py26_1",
            "test::mkl-rt-11.0-p0",
            "test::numpy-1.6.2-py26_p4",
            "test::openssl-1.0.1c-0",
            "test::pip-1.3.1-py26_1",
            "test::python-2.6.8-6",
            "test::readline-6.2-0",
            "test::scipy-0.11.0-np16py26_p3",
            "test::sqlite-3.7.13-0",
            "test::system-5.8-1",
            "test::tk-8.5.13-0",
            "test::zlib-1.2.7-0",
        }

        env.repo_packages += [
            helpers.record(
                name="pandas",
                version="0.12.0",
                build="np16py27_0",
                depends=[
                    "dateutil",
                    "numpy 1.6*",
                    "python 2.7*",
                    "pytz",
                ],
            ),
            helpers.record(
                name="numpy",
                version="1.6.2",
                build="py27_p5",
                build_number=0,
                depends=[
                    "mkl-rt 11.0",
                    "python 2.7",
                ],
                features="mkl",
            ),
        ]
        assert env.install("pandas 0.12.0 np16py27_0", "python 2.7*") == {
            "test::dateutil-2.1-py27_1",
            "test::distribute-0.6.36-py27_1",
            "test::numpy-1.6.2-py27_4",
            "test::openssl-1.0.1c-0",
            "test::pandas-0.12.0-np16py27_0",
            "test::pip-1.3.1-py27_1",
            "test::python-2.7.5-0",
            "test::pytz-2013b-py27_0",
            "test::readline-6.2-0",
            "test::six-1.3.0-py27_0",
            "test::sqlite-3.7.13-0",
            "test::system-5.8-1",
            "test::tk-8.5.13-0",
            "test::zlib-1.2.7-0",
        }
        assert env.install(
            "pandas 0.12.0 np16py27_0", "python 2.7*", MatchSpec(track_features="mkl")
        ) == {
            "test::dateutil-2.1-py27_1",
            "test::distribute-0.6.36-py27_1",
            "test::mkl-rt-11.0-p0",
            "test::numpy-1.6.2-py27_p4",
            "test::openssl-1.0.1c-0",
            "test::pandas-0.12.0-np16py27_0",
            "test::pip-1.3.1-py27_1",
            "test::python-2.7.5-0",
            "test::pytz-2013b-py27_0",
            "test::readline-6.2-0",
            "test::six-1.3.0-py27_0",
            "test::sqlite-3.7.13-0",
            "test::system-5.8-1",
            "test::tk-8.5.13-0",
            "test::zlib-1.2.7-0",
        }

    @pytest.mark.xfail(reason="CONDA_CHANNEL_PRIORITY does not seem to have any effect")
    def test_channel_priority_1(self, monkeypatch, env):
        # XXX: Test is skipped because CONDA_CHANNEL_PRIORITY does not seems to
        #      have any effect. I have also tried conda.common.io.env_var like
        #      the other tests but no luck.
        env.repo_packages = {}
        env.repo_packages["channel-A"] = []
        env.repo_packages["channel-1"] = index_packages(1)

        pandas_0 = self.find_package(
            channel="channel-1",
            name="pandas",
            version="0.10.1",
            build="np17py27_0",
        )
        env.repo_packages["channel-A"].append(pandas_0)

        # channel-1 has pandas np17py27_1, channel-A only has np17py27_0
        # when priority is set, it channel-A should take precedence and
        # np17py27_0 be installed, otherwise np17py27_1 should be installed as
        # it has a higher build version
        monkeypatch.setenv("CONDA_CHANNEL_PRIORITY", "True")
        assert "channel-A::pandas-0.11.0-np16py27_0" in env.install(
            "pandas", "python 2.7*", "numpy 1.6*"
        )
        monkeypatch.setenv("CONDA_CHANNEL_PRIORITY", "False")
        assert "channel-1::pandas-0.11.0-np16py27_1" in env.install(
            "pandas", "python 2.7*", "numpy 1.6*"
        )
        # now lets revert the channels
        env.repo_packages = dict(reversed(env.repo_packages.items()))
        monkeypatch.setenv("CONDA_CHANNEL_PRIORITY", "True")
        assert "channel-1::pandas-0.11.0-np16py27_1" in env.install(
            "pandas", "python 2.7*", "numpy 1.6*"
        )

    @pytest.mark.xfail(reason="CONDA_CHANNEL_PRIORITY does not seem to have any effect")
    def test_unsat_channel_priority(self, monkeypatch, env):
        # XXX: Test is skipped because CONDA_CHANNEL_PRIORITY does not seems to
        #      have any effect. I have also tried conda.common.io.env_var like
        #      the other tests but no luck.
        env.repo_packages = {}
        # higher priority
        env.repo_packages["channel-1"] = [
            helpers.record(
                name="a",
                version="1.0",
                depends=["c"],
            ),
            helpers.record(
                name="b",
                version="1.0",
                depends=["c >=2,<3"],
            ),
            helpers.record(
                name="c",
                version="1.0",
            ),
        ]
        # lower priority, missing c 2.0
        env.repo_packages["channel-2"] = [
            helpers.record(
                name="a",
                version="2.0",
                depends=["c"],
            ),
            helpers.record(
                name="b",
                version="2.0",
                depends=["c >=2,<3"],
            ),
            helpers.record(
                name="c",
                version="1.0",
            ),
            helpers.record(
                name="c",
                version="2.0",
            ),
        ]

        monkeypatch.setenv("CONDA_CHANNEL_PRIORITY", "True")
        records = env.install("a", "b", as_specs=True)
        # channel-1 a and b packages (1.0) installed
        assert any(k.name == "a" and k.version == "1.0" for k in records)
        assert any(k.name == "b" and k.version == "1.0" for k in records)

        monkeypatch.setenv("CONDA_CHANNEL_PRIORITY", "False")
        records = env.install("a", "b", as_specs=True)
        # no channel priority, largest version of a and b (2.0) installed
        assert any(k.name == "a" and k.version == "2.0" for k in records)
        assert any(k.name == "b" and k.version == "2.0" for k in records)

        monkeypatch.setenv("CONDA_CHANNEL_PRIORITY", "True")
        with pytest.raises(UnsatisfiableError) as exc_info:
            env.install("a", "b")
        self.assert_unsatisfiable(exc_info, [("b", "c[version='>=2,<3']")])

    @pytest.mark.xfail(
        reason="There is some weird global state making "
        "this test fail when the whole test suite is run"
    )
    def test_remove(self, env):
        env.repo_packages = index_packages(1)
        records = env.install("pandas", "python 2.7*", as_specs=True)
        assert package_string_set(records) == {
            "test::dateutil-2.1-py27_1",
            "test::distribute-0.6.36-py27_1",
            "test::numpy-1.7.1-py27_0",
            "test::openssl-1.0.1c-0",
            "test::pandas-0.11.0-np17py27_1",
            "test::pip-1.3.1-py27_1",
            "test::python-2.7.5-0",
            "test::pytz-2013b-py27_0",
            "test::readline-6.2-0",
            "test::scipy-0.12.0-np17py27_0",
            "test::six-1.3.0-py27_0",
            "test::sqlite-3.7.13-0",
            "test::system-5.8-1",
            "test::tk-8.5.13-0",
            "test::zlib-1.2.7-0",
        }

        env.installed_packages = records
        assert env.remove("pandas") == {
            "test::dateutil-2.1-py27_1",
            "test::distribute-0.6.36-py27_1",
            "test::numpy-1.7.1-py27_0",
            "test::openssl-1.0.1c-0",
            "test::pip-1.3.1-py27_1",
            "test::python-2.7.5-0",
            "test::pytz-2013b-py27_0",
            "test::readline-6.2-0",
            "test::scipy-0.12.0-np17py27_0",
            "test::six-1.3.0-py27_0",
            "test::sqlite-3.7.13-0",
            "test::system-5.8-1",
            "test::tk-8.5.13-0",
            "test::zlib-1.2.7-0",
        }
        assert env.remove("numpy") == {
            "test::dateutil-2.1-py27_1",
            "test::distribute-0.6.36-py27_1",
            "test::openssl-1.0.1c-0",
            "test::pip-1.3.1-py27_1",
            "test::python-2.7.5-0",
            "test::pytz-2013b-py27_0",
            "test::readline-6.2-0",
            "test::six-1.3.0-py27_0",
            "test::sqlite-3.7.13-0",
            "test::system-5.8-1",
            "test::tk-8.5.13-0",
            "test::zlib-1.2.7-0",
        }

    def test_surplus_features_1(self, env):
        env.repo_packages += [
            helpers.record(
                name="feature",
                track_features="feature",
            ),
            helpers.record(
                name="package1",
                features="feature",
            ),
            helpers.record(
                name="package2",
                version="1.0",
                features="feature",
                depends=["package1"],
            ),
            helpers.record(
                name="package2",
                version="2.0",
                features="feature",
            ),
        ]
        assert env.install("package2", "feature") == {
            "test::package2-2.0-0",
            "test::feature-1.0-0",
        }

    def test_surplus_features_2(self, env):
        env.repo_packages += [
            helpers.record(
                name="feature",
                track_features="feature",
            ),
            helpers.record(
                name="package1",
                features="feature",
            ),
            helpers.record(
                name="package2",
                version="1.0",
                build_number=0,
                features="feature",
                depends=["package1"],
            ),
            helpers.record(
                name="package2",
                version="1.0",
                build_number=1,
                features="feature",
            ),
        ]
        assert env.install("package2", "feature") == {
            "test::package2-1.0-0",
            "test::feature-1.0-0",
        }

    def test_get_reduced_index_broadening_with_unsatisfiable_early_dep(self, env):
        # Test that spec broadening reduction doesn't kill valid solutions
        #    In other words, the order of packages in the index should not affect the
        #    overall result of the reduced index.
        # see discussion at https://github.com/conda/conda/pull/8117#discussion_r249249815
        env.repo_packages += [
            helpers.record(
                name="a",
                version="1.0",
                # not satisfiable. This record should come first, so that its c==2
                # constraint tries to mess up the inclusion of the c record below,
                # which should be included as part of b's deps, but which is
                # broader than this dep.
                depends=["b", "c==2"],
            ),
            helpers.record(
                name="a",
                version="2.0",
                depends=["b"],
            ),
            helpers.record(
                name="b",
                depends=["c"],
            ),
            helpers.record(
                name="c",
            ),
        ]
        assert env.install("a") == {
            "test::a-2.0-0",
            "test::b-1.0-0",
            "test::c-1.0-0",
        }

    def test_get_reduced_index_broadening_preferred_solution(self, env):
        # test that order of index reduction does not eliminate what should be a preferred solution
        #    https://github.com/conda/conda/pull/8117#discussion_r249216068
        env.repo_packages += [
            helpers.record(
                name="top",
                version="1.0",
                # this is the first processed record, and imposes a broadening constraint on bottom
                #    if things are overly restricted, we'll end up with bottom 1.5 in our solution
                #    instead of the preferred (latest) 2.5
                depends=["middle", "bottom==1.5"],
            ),
            helpers.record(
                name="top",
                version="2.0",
                depends=["middle"],
            ),
            helpers.record(
                name="middle",
                depends=["bottom"],
            ),
            helpers.record(
                name="bottom",
                version="1.5",
            ),
            helpers.record(
                name="bottom",
                version="2.5",
            ),
        ]
        for record in env.install("top", as_specs=True):
            if record.name == "top":
                assert (
                    record.version == "2.0"
                ), f"top version should be 2.0, but is {record.version}"
            elif record.name == "bottom":
                assert (
                    record.version == "2.5"
                ), f"bottom version should be 2.5, but is {record.version}"

    def test_arch_preferred_over_noarch_when_otherwise_equal(self, env):
        env.repo_packages += [
            helpers.record(
                name="package1",
                subdir="noarch",
            ),
            helpers.record(
                name="package1",
            ),
        ]
        records = env.install("package1", as_specs=True)
        assert len(records) == 1
        assert records[0].subdir == context.subdir

    def test_noarch_preferred_over_arch_when_version_greater(self, env):
        env.repo_packages += [
            helpers.record(
                name="package1",
                version="2.0",
                subdir="noarch",
            ),
            helpers.record(
                name="package1",
                version="1.0",
            ),
        ]
        records = env.install("package1", as_specs=True)
        assert len(records) == 1
        assert records[0].subdir == "noarch"

    def test_noarch_preferred_over_arch_when_version_greater_dep(self, env):
        env.repo_packages += [
            helpers.record(
                name="package1",
                version="1.0",
            ),
            helpers.record(
                name="package1",
                version="2.0",
                subdir="noarch",
            ),
            helpers.record(
                name="package2",
                depends=["package1"],
            ),
        ]
        records = env.install("package2", as_specs=True)
        package1 = self.find_package_in_list(records, name="package1")
        assert package1.subdir == "noarch"

    def test_noarch_preferred_over_arch_when_build_greater(self, env):
        env.repo_packages += [
            helpers.record(
                name="package1",
                build_number=0,
            ),
            helpers.record(
                name="package1",
                build_number=1,
                subdir="noarch",
            ),
        ]
        records = env.install("package1", as_specs=True)
        assert len(records) == 1
        assert records[0].subdir == "noarch"

    def test_noarch_preferred_over_arch_when_build_greater_dep(self, env):
        env.repo_packages += [
            helpers.record(
                name="package1",
                build_number=0,
            ),
            helpers.record(
                name="package1",
                build_number=1,
                subdir="noarch",
            ),
            helpers.record(
                name="package2",
                depends=["package1"],
            ),
        ]
        records = env.install("package2", as_specs=True)
        package1 = self.find_package_in_list(records, name="package1")
        assert package1.subdir == "noarch"
