from pathlib import Path

from pkg_resources import Requirement
import pytest

from grid import dependency_manager
from grid.dependency_manager import _conda2parsed_pypi
from grid.dependency_manager import _get_rule_maps
from grid.dependency_manager import _get_system_deps
from grid.dependency_manager import _source2parsed_pypi
from grid.dependency_manager import CondaManager
from grid.dependency_manager import CondaPackage
from grid.dependency_manager import PipManager
from grid.dependency_manager import PipPackage
from grid.dependency_manager import yaml


class TestRule:
    def test_get_rule_map(self):
        rule_labels = {"pip_name_mapping", "conda_name_mapping", "system"}
        rules = _get_rule_maps()

        # verifying rule labels
        assert len(rule_labels.symmetric_difference(rules.keys())) == 0

        # verifying types for each labels
        assert isinstance(rules["pip_name_mapping"], dict)
        key = list(rules["pip_name_mapping"].keys())[0]
        assert isinstance(rules["pip_name_mapping"][key], str)

        assert isinstance(rules["conda_name_mapping"], dict)
        key = list(rules["conda_name_mapping"].keys())[0]
        assert isinstance(rules["conda_name_mapping"][key], str)

        assert isinstance(rules["system"], dict)
        key = list(rules["system"].keys())[0]
        assert isinstance(rules["system"][key], list)

    def test_hyphen_in_rules(self):
        rules = _get_rule_maps()
        for name, value in rules["pip_name_mapping"].items():
            assert "-" not in name
            assert "-" not in value

        for value in rules["conda_name_mapping"].values():
            assert "-" not in value

        for name in rules["system"]:
            assert "-" not in name

    def test_get_system_deps_missing_rule(self):
        # Should return an empty list if a rule is missing
        assert _get_system_deps("dummy_name") == []

    def test_conda2parsed_pypi_missing_rule(self):
        # Should return the same name if a name is missing
        assert _conda2parsed_pypi("dummy_name") == "dummy_name"

    def test_source2parsed_pypi_name_for_missing_rule(self):
        assert _source2parsed_pypi("dummy-name") == "dummy_name"


class TestPackageContainer:
    def test_pip_package_creation_from_string(self):
        spec = "dummy-name==0.1.0"
        pkg = PipPackage.from_string(spec)
        assert pkg.requirement_obj.url is None
        assert pkg.name == "dummy-name"
        assert pkg.parsed_name == "dummy_name"
        assert pkg.requirement_obj.specs == [("==", "0.1.0")]

        spec = "dummy@ git+https://github.com/dummy/dummy"
        pkg = PipPackage.from_string(spec)
        assert str(pkg) == "dummy@ git+https://github.com/dummy/dummy"

        spec = "-e git+git@github.com:dummy/dummy.git@myhash#egg=dummy&subdirectory=sub"
        # TODO This types of URL is generated by `python setup.py develop`.
        #  We might need to make it pass until pip 20.1
        # Catching generic exception since it behaves differently for different
        # python versions
        with pytest.raises(Exception):
            PipPackage.from_string(spec)

        with pytest.raises(Exception):
            PipPackage.from_string("invalid:requirement$string")

    def test_pip_package_creation_from_requirement(self):
        req = Requirement.parse("dummy-name==0.1.0")
        pkg = PipPackage.from_requirement(req)
        assert pkg.name == "dummy-name"
        assert pkg.parsed_name == "dummy_name"

    def test_pip_package_to_string(self):
        req = Requirement.parse(
            "dummy-name @ git+https://github.com/dummy/dummy")
        pkg = PipPackage.from_requirement(req)
        assert str(pkg) == 'dummy-name@ git+https://github.com/dummy/dummy'

        req = Requirement.parse("dummy-name")
        pkg = PipPackage.from_requirement(req)
        assert str(pkg) == "dummy-name"

        req = Requirement.parse("dummy>0.1.0,<=1.0.0")
        pkg = PipPackage.from_requirement(req)
        assert str(pkg) == "dummy<=1.0.0,>0.1.0"

    def test_pip_package_equality(self):
        pkg1 = PipPackage.from_string("dummy-name==0.1.0")
        pkg2 = PipPackage.from_string("dummy-name==0.2.0")
        assert pkg1 != pkg2

        pkg2 = CondaPackage.from_string("dummy=hash=version")
        assert pkg1 != pkg2

        pkg1 = PipPackage.from_string("dummy-name")
        pkg2 = PipPackage.from_string("dummy_name")  # underscore
        assert pkg1 == pkg2

        # version inclusion
        pkg1 = PipPackage.from_string("dummy-name>1.0,<2.0")
        pkg2 = PipPackage.from_string("dummy_name==1.5")
        assert pkg1 == pkg2
        assert pkg2 == pkg1

        pkg3 = PipPackage.from_string("dummy_name==2.5")
        assert pkg1 != pkg3

        pkg4 = PipPackage.from_string("dummy-name>1.5,<2.0")
        assert pkg1 != pkg4  # supposed to be write

        pkg1 = PipPackage.from_string("dummy-name==2.0")
        pkg2 = PipPackage.from_string("dummy-name")
        pkg3 = PipPackage.from_string("dummy-name>=")
        # first specifier has empty version
        pkg4 = PipPackage.from_string("dummy-name>=, <3.0")
        assert pkg1 != pkg2
        assert pkg1 != pkg3
        assert pkg1 == pkg4

    def test_conda_package_creation(self):
        pkg = CondaPackage(name="dummy-name", version="0.1.0", build="py1000")
        assert pkg.parsed_name == "dummy_name"

        pkg = CondaPackage.from_string("dummy-name=0.1.0=py1000")
        assert pkg.build == "py1000"

        with pytest.raises(ValueError, match="Arguments cannot be undefined"):
            CondaPackage.from_string("dummy==0.1")

        with pytest.raises(RuntimeError, match="Error while parsing"):
            CondaPackage.from_string("dummy=0.1.0")

        with pytest.raises(RuntimeError, match="Error while parsing"):
            CondaPackage.from_string("dummy=0.1.0=py1000=fake")

    def test_conda_package_to_string(self):
        pkg = CondaPackage(name="dummy-name", version="0.1.0", build="py1000")
        assert str(pkg) == "dummy-name=0.1.0=py1000"

    def test_conda_package_equality(self):
        pkg1 = CondaPackage(name="dummy-name", version="0.1.0", build="py100")
        pkg2 = CondaPackage(name="dummy-name", version="0.1.0", build="py100")
        pkg3 = PipPackage.from_string("dummy-name")
        assert pkg1 == pkg2
        assert pkg1 != pkg3

        pkg1.build = "another build"
        assert pkg1 != pkg2


class TestDepsManagerBaseClass:
    def test_read_file_deps_with_no_requirement_file(self, monkeypatch):
        monkeypatch.setattr(PipManager, "_setup", lambda x: None)
        pm = PipManager()
        pm.req_file = None
        monkeypatch.setattr(pm, "req_file", Path("filedoesnotexist"))
        assert pm._read_file_deps() == {}

    def test_get_missing(self, monkeypatch):
        source_deps = ["pkg_a", "pkg_b", "pkg_c"]

        def test_setup(self):
            self.env_deps = {
                "pkg_z": object(),
                "pkg_a": object(),
                "pkg_b": object()
            }
            self.file_deps = {
                "pkg_b": object(),
                "pkg_c": object(),
                "pkg_d": object()
            }
            self._source_deps_list = source_deps

        # No missing keys
        monkeypatch.setattr(PipManager, "_setup", test_setup)
        pm = PipManager()
        assert set() == pm.get_missing()

        # adding missing keys
        source_deps.append("pkg_missing")
        assert {"pkg_missing"} == pm.get_missing()

    def test_has_change(self, monkeypatch):
        source_deps = ["pkg_a", "pkg_b"]

        class DummyFile:
            def __init__(self):
                self.exists = lambda: False

        dummyfile = DummyFile()

        def test_setup(self):
            self.req_file = dummyfile
            self.env_deps = {
                "pkg_z": object(),
                "pkg_a": object(),
                "pkg_b": object()
            }
            self.file_deps = {
                "pkg_b": object(),
                "pkg_c": object(),
                "pkg_d": object()
            }
            self._source_deps_list = source_deps

        # No requirement file exist
        monkeypatch.setattr(PipManager, "_setup", test_setup)
        pm = PipManager()
        assert pm.has_change is True

        # requirement file exist but keys from source is not present in file deps
        dummyfile.exists = lambda: True
        assert pm.has_change is True

        # keys from source is not in env deps which is OK (pkg_d)
        # env deps package != file deps package (pkg_b)
        source_deps.clear()
        source_deps.append("pkg_d")
        source_deps.append("pkg_b")
        assert pm.has_change is True

    def test_collate_final(self, monkeypatch):
        source_deps = ["pkg_a", "pkg_b", "pkg_c", "pkg_d"]
        obja, objb, objc, objd = object(), object(), object(), object()

        def test_setup(self):
            self.env_deps = {"pkg_d": objd}
            self.file_deps = {"pkg_c": objc, "pkg_b": objb, "pkg_a": obja}
            self._source_deps_list = source_deps

        # Adding missing deps & keeping the order of elements in file deps
        monkeypatch.setattr(PipManager, "_setup", test_setup)
        pm = PipManager()
        assert pm.collate_final() == [objc, objb, obja, objd]

        # Updating deps from env if env deps is not same as file deps
        newobjc = object()
        pm.env_deps = {"pkg_d": objd, "pkg_c": newobjc}
        assert pm.collate_final() == [newobjc, objb, obja, objd]

    def test_write_config(self, monkeypatch):
        source_deps = ["pkg_a", "pkg_b", "pkg_c", "pkg_d"]
        # monkeypatching safedump to ease testing
        yamlout = []
        monkeypatch.setattr(yaml, "safe_dump", lambda c, f: yamlout.append(
            (c, f)))

        def test_setup(self):
            self._source_deps_list = source_deps

        # No system deps found
        monkeypatch.setattr(PipManager, "_setup", test_setup)
        pm = PipManager()
        assert pm.write_config({"compute": {}}) is None

        # Empty config
        monkeypatch.setattr(dependency_manager, "_get_system_deps",
                            lambda x: [x])
        with pytest.raises(KeyError, match="compute"):
            pm.write_config({})

        # multiple system deps
        pm.write_config({"compute": {}})
        assert yamlout[0][0] == {
            'compute': {
                'actions': {
                    'on_image_build':
                    ['apt install -y pkg_a pkg_b pkg_c pkg_d']
                }
            }
        }

        configfile = yamlout[0][1]
        configfile.close()
        Path(configfile.name).unlink()

        yamlout.clear()
        pm.write_config({"compute": {"actions": {"on_image_build": []}}})
        assert yamlout[0][0] == {
            'compute': {
                'actions': {
                    'on_image_build':
                    ['apt install -y pkg_a pkg_b pkg_c pkg_d']
                }
            }
        }

        configfile = yamlout[0][1]
        configfile.close()
        Path(configfile.name).unlink()


class TestPipManager:
    def test_read_file_deps_success(self, monkeypatch):
        class DummyFile:
            def __init__(self):
                self.exists = lambda: True
                self.read_text = lambda: "pkg-a==0.2\npkg-b"

        monkeypatch.setattr(PipManager, "_setup", lambda x: None)
        pm = PipManager()
        pm.req_file = None
        dummyfile = DummyFile()
        monkeypatch.setattr(pm, "req_file", dummyfile)
        final = {
            "pkg_a": PipPackage.from_string("pkg-a==0.2"),
            "pkg_b": PipPackage.from_string("pkg-b")
        }
        assert pm._read_file_deps() == final

    def test_read_file_deps_with_invalid_file(self, monkeypatch):
        class DummyFile:
            def __init__(self):
                self.exists = lambda: True
                self.read_text = lambda: "channel:\n  - pytorch\n"

        monkeypatch.setattr(PipManager, "_setup", lambda x: None)
        pm = PipManager()
        pm.req_file = None
        dummyfile = DummyFile()
        monkeypatch.setattr(pm, "req_file", dummyfile)
        with pytest.raises(Exception):
            # generic exception since different python version raises
            # different exception
            pm._read_file_deps()

    def test_get_default_file(self, monkeypatch):
        monkeypatch.setattr(PipManager, "_setup", lambda x: None)
        pm = PipManager()
        assert Path("requirements.txt") == pm._get_default_req_file()


class TestCondaManager:
    def test_read_file_deps_success(self, monkeypatch):
        class DummyFile:
            def __init__(self):
                self.exists = lambda: True
                self.read_text = lambda: 'channels:\n  - pytorch\n  - defaults\ndependencies:\n  - pkg-b=20.1.0=py38\n  - pkg-a=1.10=py_0\n'

        monkeypatch.setattr(CondaManager, "_setup", lambda x: None)
        cm = CondaManager()
        cm.req_file = None
        dummyfile = DummyFile()
        monkeypatch.setattr(cm, "req_file", dummyfile)
        final = {
            "pkg_b": CondaPackage.from_string("pkg-b=20.1.0=py38"),
            "pkg_a": CondaPackage.from_string("pkg-a=1.10=py_0")
        }
        assert cm._read_file_deps() == final

    def test_read_file_deps_with_invalid_file(self, monkeypatch):
        class DummyFile:
            def __init__(self):
                self.exists = lambda: True
                self.read_text = lambda: "pkg-a==0.2\npkg-b"

        monkeypatch.setattr(CondaManager, "_setup", lambda x: None)
        pm = CondaManager()
        pm.req_file = None
        dummyfile = DummyFile()
        monkeypatch.setattr(pm, "req_file", dummyfile)
        with pytest.raises(RuntimeError):
            # generic exception since different python version raises
            # different exception
            pm._read_file_deps()

    def test_get_default_file(self, monkeypatch):
        monkeypatch.setattr(CondaManager, "_setup", lambda x: None)
        pm = CondaManager()
        assert Path("environment.yml") == pm._get_default_req_file()
