from __future__ import unicode_literals

import os
import platform
import stat
import subprocess
import sys

import py
import pytest

import tox
from tox import reporter
from tox.config import get_plugin_manager
from tox.interpreters import (
    ExecFailed,
    InterpreterInfo,
    Interpreters,
    NoInterpreterInfo,
    run_and_get_interpreter_info,
    tox_get_python_executable,
)
from tox.reporter import Verbosity


@pytest.fixture(name="interpreters")
def create_interpreters_instance():
    pm = get_plugin_manager()
    return Interpreters(hook=pm.hook)


@pytest.mark.skipif(tox.INFO.IS_PYPY, reason="testing cpython interpreter discovery")
def test_tox_get_python_executable(mocker):
    class envconfig:
        basepython = sys.executable
        envname = "pyxx"
        config = mocker.MagicMock()
        config.return_value.option.return_value.discover = []

    def get_exe(name):
        envconfig.basepython = name
        p = tox_get_python_executable(envconfig)
        assert p
        return str(p)

    def assert_version_in_output(exe, version):
        out = subprocess.check_output((exe, "-V"), stderr=subprocess.STDOUT)
        assert version in out.decode()

    p = tox_get_python_executable(envconfig)
    assert p == py.path.local(sys.executable)
    for major, minor in [(2, 7), (3, 5), (3, 6), (3, 7), (3, 8)]:
        name = "python{}.{}".format(major, minor)
        if tox.INFO.IS_WIN:
            pydir = "python{}{}".format(major, minor)
            x = py.path.local(r"c:\{}".format(pydir))
            if not x.check():
                continue
        else:
            if not py.path.local.sysfind(name) or subprocess.call((name, "-c", "")):
                continue
        exe = get_exe(name)
        assert_version_in_output(exe, "{}.{}".format(major, minor))
    has_py_exe = py.path.local.sysfind("py") is not None
    for major in (2, 3):
        name = "python{}".format(major)
        if has_py_exe:
            error_code = subprocess.call(("py", "-{}".format(major), "-c", ""))
            if error_code:
                continue
        elif not py.path.local.sysfind(name):
            continue

        exe = get_exe(name)
        assert_version_in_output(exe, str(major))


@pytest.mark.skipif("sys.platform == 'win32'", reason="symlink execution unreliable on Windows")
def test_find_alias_on_path(monkeypatch, tmp_path, mocker):
    reporter.update_default_reporter(Verbosity.DEFAULT, Verbosity.DEBUG)
    magic = tmp_path / "magic{}".format(os.path.splitext(sys.executable)[1])
    os.symlink(sys.executable, str(magic))
    monkeypatch.setenv(
        str("PATH"),
        os.pathsep.join([str(tmp_path)] + os.environ.get(str("PATH"), "").split(os.pathsep)),
    )

    class envconfig:
        basepython = "magic"
        envname = "pyxx"
        config = mocker.MagicMock()
        config.return_value.option.return_value.discover = []

    detected = py.path.local.sysfind("magic")
    assert detected

    t = tox_get_python_executable(envconfig).lower()
    assert t == str(magic).lower()


def test_run_and_get_interpreter_info():
    name = os.path.basename(sys.executable)
    info = run_and_get_interpreter_info(name, sys.executable)
    assert info.version_info == tuple(sys.version_info)
    assert info.implementation == platform.python_implementation()
    assert info.executable == sys.executable


class TestInterpreters:
    def test_get_executable(self, interpreters, mocker):
        class envconfig:
            basepython = sys.executable
            envname = "pyxx"
            config = mocker.MagicMock()
            config.return_value.option.return_value.discover = []

        x = interpreters.get_executable(envconfig)
        assert x == sys.executable
        info = interpreters.get_info(envconfig)
        assert info.version_info == tuple(sys.version_info)
        assert info.executable == sys.executable
        assert isinstance(info, InterpreterInfo)

    def test_get_executable_no_exist(self, interpreters, mocker):
        class envconfig:
            basepython = "1lkj23"
            envname = "pyxx"
            config = mocker.MagicMock()
            config.return_value.option.return_value.discover = []

        assert not interpreters.get_executable(envconfig)
        info = interpreters.get_info(envconfig)
        assert not info.version_info
        assert info.name == "1lkj23"
        assert not info.executable
        assert isinstance(info, NoInterpreterInfo)

    @pytest.mark.skipif("sys.platform == 'win32'", reason="Uses a unix only wrapper")
    def test_get_info_uses_hook_path(self, tmp_path):
        magic = tmp_path / "magic{}".format(os.path.splitext(sys.executable)[1])
        wrapper = (
            "#!{executable}\n"
            "import subprocess\n"
            "import sys\n"
            'sys.exit(subprocess.call(["{executable}"] + sys.argv[1:]))\n'
        ).format(executable=sys.executable)
        magic.write_text(wrapper)
        magic.chmod(magic.stat().st_mode | stat.S_IEXEC)

        class MockHook:
            def tox_get_python_executable(self, envconfig):
                return str(magic)

        class envconfig:
            basepython = sys.executable
            envname = "magicpy"

        # Check that the wrapper is working first.
        # If it isn't, the default is to return the passed path anyway.
        subprocess.check_call([str(magic), "--help"])

        interpreters = Interpreters(hook=MockHook())
        info = interpreters.get_info(envconfig)
        assert info.executable == str(magic)

    def test_get_sitepackagesdir_error(self, interpreters, mocker):
        class envconfig:
            basepython = sys.executable
            envname = "123"
            config = mocker.MagicMock()
            config.return_value.option.return_value.discover = []

        info = interpreters.get_info(envconfig)
        s = interpreters.get_sitepackagesdir(info, "")
        assert s


def test_exec_failed():
    x = ExecFailed("my-executable", "my-source", "my-out", "my-err")
    assert isinstance(x, Exception)
    assert x.executable == "my-executable"
    assert x.source == "my-source"
    assert x.out == "my-out"
    assert x.err == "my-err"


class TestInterpreterInfo:
    @staticmethod
    def info(
        implementation="CPython",
        executable="my-executable",
        version_info="my-version-info",
        sysplatform="my-sys-platform",
    ):
        return InterpreterInfo(implementation, executable, version_info, sysplatform, True, None)

    def test_data(self):
        x = self.info("larry", "moe", "shemp", "curly")
        assert x.implementation == "larry"
        assert x.executable == "moe"
        assert x.version_info == "shemp"
        assert x.sysplatform == "curly"

    def test_str(self):
        x = self.info(executable="foo", version_info="bar")
        assert str(x) == "<executable at foo, version_info bar>"


class TestNoInterpreterInfo:
    def test_default_data(self):
        x = NoInterpreterInfo("foo")
        assert x.name == "foo"
        assert x.executable is None
        assert x.version_info is None
        assert x.out is None
        assert x.err == "not found"

    def test_set_data(self):
        x = NoInterpreterInfo("migraine", executable="my-executable", out="my-out", err="my-err")
        assert x.name == "migraine"
        assert x.executable == "my-executable"
        assert x.version_info is None
        assert x.out == "my-out"
        assert x.err == "my-err"

    def test_str_without_executable(self):
        x = NoInterpreterInfo("coconut")
        assert str(x) == "<executable not found for: coconut>"

    def test_str_with_executable(self):
        x = NoInterpreterInfo("coconut", executable="bang/em/together")
        assert str(x) == "<executable at bang/em/together, not runnable>"