import os
import platform
import shutil
import sys
from pathlib import Path

import pytest

# Need to import everything to get fixtures
from .helpers import *  # noqa: F403
from . import helpers


@pytest.mark.skipif(
    helpers.dry_run_tests == helpers.DryRun.ULTRA_DRY, reason="Running ultra dry tests"
)
class TestUpdate:
    current_root_prefix = os.environ["MAMBA_ROOT_PREFIX"]
    current_prefix = os.environ["CONDA_PREFIX"]

    env_name = helpers.random_string()
    root_prefix = os.path.expanduser(os.path.join("~", "tmproot" + helpers.random_string()))
    prefix = os.path.join(root_prefix, "envs", env_name)
    old_version = "0.21.10"
    medium_old_version = "0.22"

    @staticmethod
    @pytest.fixture(scope="class")
    def root(existing_cache):
        os.environ["MAMBA_ROOT_PREFIX"] = TestUpdate.root_prefix
        os.environ["CONDA_PREFIX"] = TestUpdate.prefix

        yield

        os.environ["MAMBA_ROOT_PREFIX"] = TestUpdate.current_root_prefix
        os.environ["CONDA_PREFIX"] = TestUpdate.current_prefix
        shutil.rmtree(TestUpdate.root_prefix)

    @staticmethod
    @pytest.fixture
    def env_created(root):
        if helpers.dry_run_tests == helpers.DryRun.OFF:
            helpers.create(
                f"xtensor={TestUpdate.old_version}",
                "-n",
                TestUpdate.env_name,
                "--json",
                no_dry_run=True,
            )
        res = helpers.umamba_list("xtensor", "-n", TestUpdate.env_name, "--json")
        assert len(res) == 1
        assert res[0]["version"].startswith(TestUpdate.old_version)

        yield TestUpdate.env_name

        shutil.rmtree(TestUpdate.prefix)

    def test_constrained_update(self, env_created):
        update_res = helpers.update(
            "xtensor<=" + self.medium_old_version, "-n", env_created, "--json"
        )
        xtensor_link = [
            to_link for to_link in update_res["actions"]["LINK"] if to_link["name"] == "xtensor"
        ][0]

        assert xtensor_link["version"].startswith(self.medium_old_version)

    # test that we relink noarch packages
    def test_update_python_noarch(self, root):
        if helpers.dry_run_tests == helpers.DryRun.OFF:
            helpers.create(
                "python=3.9",
                "six",
                "requests",
                "-n",
                TestUpdate.env_name,
                "--json",
                no_dry_run=True,
            )
        else:
            return

        res = helpers.umamba_list("python", "-n", TestUpdate.env_name, "--json")
        assert len(res) >= 1
        pyelem = [r for r in res if r["name"] == "python"][0]
        assert pyelem["version"].startswith("3.9")

        res = helpers.umamba_list("requests", "-n", TestUpdate.env_name, "--json")
        prev_requests = [r for r in res if r["name"] == "requests"][0]
        assert prev_requests["version"]

        def site_packages_path(p, pyver):
            if platform.system() == "Windows":
                return os.path.join(self.prefix, "Lib\\site-packages\\", p)
            else:
                return os.path.join(self.prefix, f"lib/python{pyver}/site-packages", p)

        assert os.path.exists(site_packages_path("requests/__pycache__", "3.9"))

        prev_six = helpers.umamba_list("six", "-n", TestUpdate.env_name, "--json")[0]

        update_res = helpers.update("-n", TestUpdate.env_name, "python=3.10", "--json")

        six_link = [
            to_link for to_link in update_res["actions"]["LINK"] if to_link["name"] == "six"
        ][0]

        assert six_link["version"] == prev_six["version"]
        assert six_link["build_string"] == prev_six["build_string"]

        requests_link = [
            to_link for to_link in update_res["actions"]["LINK"] if to_link["name"] == "requests"
        ][0]
        requests_unlink = [
            to_link for to_link in update_res["actions"]["UNLINK"] if to_link["name"] == "requests"
        ][0]

        assert requests_link["version"] == requests_unlink["version"]
        if platform.system() != "Windows":
            assert not os.path.exists(site_packages_path("", "3.9"))
        assert os.path.exists(site_packages_path("requests/__pycache__", "3.10"))

        assert requests_link["version"] == prev_requests["version"]
        assert requests_link["build_string"] == prev_requests["build_string"]

    def test_further_constrained_update(self, env_created):
        update_res = helpers.update("xtensor==0.24.5=*_0", "--json")
        xtensor_link = [
            to_link for to_link in update_res["actions"]["LINK"] if to_link["name"] == "xtensor"
        ][0]

        assert xtensor_link["version"] == "0.24.5"
        assert xtensor_link["build_number"] == 0

    def test_classic_spec(self, env_created):
        update_res = helpers.update("xtensor", "--json", "-n", TestUpdate.env_name)

        xtensor_link = [
            to_link for to_link in update_res["actions"]["LINK"] if to_link["name"] == "xtensor"
        ][0]
        assert TestUpdate.old_version != xtensor_link["version"]

        if helpers.dry_run_tests == helpers.DryRun.OFF:
            pkg = helpers.get_concrete_pkg(update_res, "xtensor")
            pkg_info = helpers.get_concrete_pkg_info(helpers.get_env(TestUpdate.env_name), pkg)
            version = pkg_info["version"]

            assert TestUpdate.old_version != version

        # This should do nothing since python is not installed!
        update_res = helpers.update("python", "-n", TestUpdate.env_name, "--json")

        # TODO fix this?!
        assert update_res["message"] == "All requested packages already installed"
        assert update_res["success"] is True
        assert "action" not in update_res

    def test_update_all(self, env_created):
        update_res = helpers.update("--all", "--json")

        xtensor_link = [
            to_link for to_link in update_res["actions"]["LINK"] if to_link["name"] == "xtensor"
        ][0]
        assert TestUpdate.old_version != xtensor_link["version"]

        if helpers.dry_run_tests == helpers.DryRun.OFF:
            pkg = helpers.get_concrete_pkg(update_res, "xtensor")
            pkg_info = helpers.get_concrete_pkg_info(helpers.get_env(TestUpdate.env_name), pkg)
            version = pkg_info["version"]

            assert TestUpdate.old_version != version

            with open(Path(self.prefix) / "conda-meta" / "history") as h:
                history = h.readlines()
            print("".join(history))
            for el in reversed(history):
                x = el.strip()
                if x.startswith(">=="):
                    break
                assert not x.startswith("update specs:")

    @pytest.mark.parametrize(
        "alias",
        [
            None,
            "https://conda.anaconda.org/",
            "https://repo.mamba.pm/",
            "https://repo.mamba.pm",
        ],
    )
    def test_channel_alias(self, alias, env_created):
        if alias:
            res = helpers.update(
                "-n",
                TestUpdate.env_name,
                "xtensor",
                "--json",
                "--dry-run",
                "--channel-alias",
                alias,
            )
        else:
            res = helpers.update("-n", TestUpdate.env_name, "xtensor", "--json", "--dry-run")

        for to_link in res["actions"]["LINK"]:
            assert to_link["channel"] == "conda-forge"

    @pytest.mark.parametrize("output_flag", ["", "--json", "--quiet"])
    def test_update_check_logs(self, env_created, output_flag):
        res = helpers.update("-n", TestUpdate.env_name, "xtensor=0.24.5", output_flag)

        if output_flag == "--json":
            assert res["success"]
        elif output_flag == "--quiet":
            assert res == ""
        else:
            assert "To activate this environment, use:" not in res

    def test_update_explains_problems(self, env_created):
        # Non-regression test for: https://github.com/mamba-org/mamba/issues/3828
        with pytest.raises(helpers.subprocess.CalledProcessError) as e:
            helpers.update("-n", TestUpdate.env_name, "xtensor=0.24.5", "xtensor=0.25.0")
        err_string = str(e.value.stderr.decode("utf-8"))
        assert "The following packages are incompatible" in err_string

    def test_update_explains_problems_json(self, env_created):
        with pytest.raises(helpers.subprocess.CalledProcessError) as e:
            helpers.update("-n", TestUpdate.env_name, "xtensor=0.24.5", "xtensor=0.25.0", "--json")
        out_string = str(e.value.stdout.decode("utf-8"))
        assert "cannot install both" in out_string


class TestUpdateConfig:
    current_root_prefix = os.environ["MAMBA_ROOT_PREFIX"]
    current_prefix = os.environ["CONDA_PREFIX"]

    env_name = helpers.random_string()
    root_prefix = os.path.expanduser(os.path.join("~", "tmproot" + helpers.random_string()))
    prefix = os.path.join(root_prefix, "envs", env_name)

    @staticmethod
    @pytest.fixture(scope="class")
    def root(existing_cache):
        os.environ["MAMBA_ROOT_PREFIX"] = TestUpdateConfig.root_prefix
        os.environ["CONDA_PREFIX"] = TestUpdateConfig.prefix
        helpers.create("-n", "base", no_dry_run=True)
        helpers.create("-n", TestUpdateConfig.env_name, "--offline", no_dry_run=True)

        yield

        os.environ["MAMBA_ROOT_PREFIX"] = TestUpdateConfig.current_root_prefix
        os.environ["CONDA_PREFIX"] = TestUpdateConfig.current_prefix
        shutil.rmtree(TestUpdateConfig.root_prefix)

    @staticmethod
    @pytest.fixture
    def env_created(root):
        os.environ["MAMBA_ROOT_PREFIX"] = TestUpdateConfig.root_prefix
        os.environ["CONDA_PREFIX"] = TestUpdateConfig.prefix

        yield

        for v in ("CONDA_CHANNELS", "MAMBA_TARGET_PREFIX"):
            if v in os.environ:
                os.environ.pop(v)

    @classmethod
    def config_tests(cls, res, root_prefix=root_prefix, target_prefix=prefix):
        assert res["root_prefix"] == root_prefix
        assert res["target_prefix"] == target_prefix
        assert res["use_target_prefix_fallback"]
        assert res["use_default_prefix_fallback"]
        assert res["use_root_prefix_fallback"]
        checks = (
            helpers.MAMBA_ALLOW_EXISTING_PREFIX
            | helpers.MAMBA_NOT_ALLOW_MISSING_PREFIX
            | helpers.MAMBA_NOT_ALLOW_NOT_ENV_PREFIX
            | helpers.MAMBA_EXPECT_EXISTING_PREFIX
        )
        assert res["target_prefix_checks"] == checks

    @pytest.mark.parametrize(
        "source,file_type",
        [
            ("cli_only", None),
            ("spec_file_only", "classic"),
            ("spec_file_only", "explicit"),
            ("spec_file_only", "yaml"),
            ("both", "classic"),
            ("both", "explicit"),
            ("both", "yaml"),
        ],
    )
    def test_specs(self, source, file_type, env_created):
        cmd = []
        specs = []

        if source in ("cli_only", "both"):
            specs = ["xtensor-python", "xtl"]
            cmd = list(specs)

        if source in ("spec_file_only", "both"):
            f_name = helpers.random_string()
            spec_file = os.path.join(TestUpdateConfig.root_prefix, f_name)

            if file_type == "classic":
                file_content = ["xtensor >0.20", "xsimd"]
                specs += file_content
            elif file_type == "explicit":
                explicit_specs = [
                    "https://conda.anaconda.org/conda-forge/linux-64/xtensor-0.21.5-hc9558a2_0.tar.bz2#d330e02e5ed58330638a24601b7e4887",
                    "https://conda.anaconda.org/conda-forge/linux-64/xsimd-7.4.8-hc9558a2_0.tar.bz2#32d5b7ad7d6511f1faacf87e53a63e5f",
                ]
                file_content = ["@EXPLICIT"] + explicit_specs
                specs = explicit_specs
            else:  # yaml
                spec_file += ".yaml"
                file_content = ["dependencies:", "  - xtensor >0.20", "  - xsimd"]
                specs += ["xtensor >0.20", "xsimd"]

            with open(spec_file, "w") as f:
                f.write("\n".join(file_content))

            cmd += ["-f", spec_file]

        res = helpers.install(*cmd, "--print-config-only")

        TestUpdateConfig.config_tests(res)
        assert res["env_name"] == ""
        assert res["specs"] == specs

    @pytest.mark.parametrize("root_prefix", (None, "env_var", "cli"))
    @pytest.mark.parametrize("target_is_root", (False, True))
    @pytest.mark.parametrize("cli_prefix", (False, True))
    @pytest.mark.parametrize("cli_env_name", (False, True))
    @pytest.mark.parametrize("yaml_name", (False, True, "prefix"))
    @pytest.mark.parametrize("env_var", (False, True))
    @pytest.mark.parametrize("current_target_prefix_fallback", (False, True))
    def test_target_prefix(
        self,
        root_prefix,
        target_is_root,
        cli_prefix,
        cli_env_name,
        yaml_name,
        env_var,
        current_target_prefix_fallback,
        env_created,
    ):
        cmd = []

        if root_prefix in (None, "cli"):
            os.environ["MAMBA_DEFAULT_ROOT_PREFIX"] = os.environ.pop("MAMBA_ROOT_PREFIX")

        if root_prefix == "cli":
            cmd += ["-r", TestUpdateConfig.root_prefix]

        r = TestUpdateConfig.root_prefix

        if target_is_root:
            p = r
            n = "base"
        else:
            p = TestUpdateConfig.prefix
            n = TestUpdateConfig.env_name

        expected_p = p

        if cli_prefix:
            cmd += ["-p", p]

        if cli_env_name:
            cmd += ["-n", n]

        if yaml_name:
            f_name = helpers.random_string() + ".yaml"
            spec_file = os.path.join(TestUpdateConfig.prefix, f_name)

            if yaml_name == "prefix":
                yaml_n = p
            else:
                yaml_n = n
                if not (cli_prefix or cli_env_name or target_is_root):
                    expected_p = os.path.join(TestUpdateConfig.root_prefix, "envs", yaml_n)

            file_content = [
                f"name: {yaml_n}",
                "dependencies: [xtensor]",
            ]
            with open(spec_file, "w") as f:
                f.write("\n".join(file_content))

            cmd += ["-f", spec_file]

        if env_var:
            os.environ["MAMBA_TARGET_PREFIX"] = p

        if not current_target_prefix_fallback:
            os.environ.pop("CONDA_PREFIX")
            os.environ.pop("CONDA_DEFAULT_ENV")
        else:
            os.environ["CONDA_PREFIX"] = p

        if (cli_prefix and cli_env_name) or (yaml_name == "prefix"):
            with pytest.raises(helpers.subprocess.CalledProcessError):
                helpers.install(*cmd, "--print-config-only")
        elif not (
            cli_prefix or cli_env_name or yaml_name or env_var or current_target_prefix_fallback
        ):
            # Fallback on root prefix
            res = helpers.install(*cmd, "--print-config-only")
            TestUpdateConfig.config_tests(res, root_prefix=r, target_prefix=r)
        else:
            res = helpers.install(*cmd, "--print-config-only")
            TestUpdateConfig.config_tests(res, root_prefix=r, target_prefix=expected_p)

    def test_target_prefix_with_no_settings(
        self,
        existing_cache,
    ):
        # Specify no arg
        cmd = []

        # Get the actual set MAMBA_ROOT_PREFIX when setting up `TestUpdateConfig` class
        os.environ["MAMBA_DEFAULT_ROOT_PREFIX"] = os.environ.pop("MAMBA_ROOT_PREFIX")
        os.environ.pop("CONDA_PREFIX")
        os.environ.pop("CONDA_DEFAULT_ENV")

        # Fallback on root prefix
        res = helpers.install(*cmd, "--print-config-only")
        TestUpdateConfig.config_tests(
            res,
            root_prefix=TestUpdateConfig.root_prefix,
            target_prefix=TestUpdateConfig.root_prefix,
        )

    @pytest.mark.skipif(
        sys.platform == "win32",
        reason="MAMBA_ROOT_PREFIX is set in windows GH workflow",
    )
    def test_target_prefix_with_no_settings_and_no_env_var(
        self,
        existing_cache,
    ):
        # Specify no arg
        cmd = []

        os.environ.pop("MAMBA_ROOT_PREFIX")
        os.environ.pop("CONDA_PREFIX")
        os.environ.pop("CONDA_DEFAULT_ENV")

        # Fallback on root prefix
        res = helpers.install(*cmd, "--print-config-only")

        TestUpdateConfig.config_tests(
            res,
            root_prefix=TestUpdateConfig.current_root_prefix,
            target_prefix=TestUpdateConfig.current_root_prefix,
        )

    @pytest.mark.parametrize("cli", (False, True))
    @pytest.mark.parametrize("yaml", (False, True))
    @pytest.mark.parametrize("env_var", (False, True))
    @pytest.mark.parametrize("rc_file", (False, True))
    def test_channels(self, cli, yaml, env_var, rc_file, env_created):
        cmd = []
        expected_channels = []

        if cli:
            cmd += ["-c", "cli"]
            expected_channels += ["cli"]

        if yaml:
            f_name = helpers.random_string() + ".yaml"
            spec_file = os.path.join(TestUpdateConfig.prefix, f_name)

            file_content = [
                "channels: [yaml]",
                "dependencies: [xtensor]",
            ]
            with open(spec_file, "w") as f:
                f.write("\n".join(file_content))

            cmd += ["-f", spec_file]
            expected_channels += ["yaml"]

        if env_var:
            os.environ["CONDA_CHANNELS"] = "env_var"
            expected_channels += ["env_var"]

        if rc_file:
            f_name = helpers.random_string() + ".yaml"
            rc_file = os.path.join(TestUpdateConfig.prefix, f_name)

            file_content = ["channels: [rc]"]
            with open(rc_file, "w") as f:
                f.write("\n".join(file_content))

            cmd += ["--rc-file", rc_file]
            expected_channels += ["rc"]

        res = helpers.install(*cmd, "--print-config-only", no_rc=not rc_file, default_channel=False)
        TestUpdateConfig.config_tests(res)
        if expected_channels:
            assert res["channels"] == expected_channels
        else:
            assert res["channels"] == ["conda-forge"]

    @pytest.mark.parametrize("type", ("yaml", "classic", "explicit"))
    def test_multiple_spec_files(self, type, env_created):
        cmd = []
        specs = ["xtensor", "xsimd"]
        explicit_specs = [
            "https://conda.anaconda.org/conda-forge/linux-64/xtensor-0.21.5-hc9558a2_0.tar.bz2#d330e02e5ed58330638a24601b7e4887",
            "https://conda.anaconda.org/conda-forge/linux-64/xsimd-7.4.8-hc9558a2_0.tar.bz2#32d5b7ad7d6511f1faacf87e53a63e5f",
        ]

        for i in range(2):
            f_name = helpers.random_string()
            file = os.path.join(TestUpdateConfig.prefix, f_name)

            if type == "yaml":
                file += ".yaml"
                file_content = [f"dependencies: [{specs[i]}]"]
            elif type == "classic":
                file_content = [specs[i]]
            else:  # explicit
                file_content = ["@EXPLICIT", explicit_specs[i]]

            with open(file, "w") as f:
                f.write("\n".join(file_content))

            cmd += ["-f", file]

        res = helpers.install(*cmd, "--print-config-only")
        if type == "yaml" or type == "classic":
            assert res["specs"] == specs
        else:  # explicit
            assert res["specs"] == [explicit_specs[0]]

    def test_channel_specific(self, env_created):
        helpers.install("quantstack::sphinx", no_dry_run=True)
        res = helpers.update("quantstack::sphinx", "-c", "conda-forge", "--json")
        assert "actions" not in res
