import json
import os
import tarfile
import zipfile
from os.path import abspath, dirname
from os.path import join as pjoin
from unittest.mock import Mock

import pytest
import tomli
from testpath import assert_isfile, modified_env
from testpath.tempdir import TemporaryDirectory, TemporaryWorkingDirectory

from pyproject_hooks.wrappers import (
    BackendUnavailable,
    PyProjectHookCaller,
    UnsupportedOperation,
    default_subprocess_runner,
)

SAMPLES_DIR = pjoin(dirname(abspath(__file__)), "samples")
BUILDSYS_PKGS = pjoin(SAMPLES_DIR, "buildsys_pkgs")


def get_hooks(pkg, **kwargs):
    source_dir = pjoin(SAMPLES_DIR, pkg)
    with open(pjoin(source_dir, "pyproject.toml"), "rb") as f:
        data = tomli.load(f)
    return PyProjectHookCaller(
        source_dir, data["build-system"]["build-backend"], **kwargs
    )


def test_missing_backend_gives_exception():
    hooks = get_hooks("pkg1")
    with modified_env({"PYTHONPATH": ""}):
        with pytest.raises(BackendUnavailable):
            hooks.get_requires_for_build_wheel({})


def test_get_requires_for_build_wheel():
    hooks = get_hooks("pkg1")
    with modified_env({"PYTHONPATH": BUILDSYS_PKGS}):
        res = hooks.get_requires_for_build_wheel({})
    assert res == ["wheelwright"]


def test_get_requires_for_build_editable():
    hooks = get_hooks("pkg1")
    with modified_env({"PYTHONPATH": BUILDSYS_PKGS}):
        res = hooks.get_requires_for_build_editable({})
    assert res == ["wheelwright", "editables"]


def test_get_requires_for_build_sdist():
    hooks = get_hooks("pkg1")
    with modified_env({"PYTHONPATH": BUILDSYS_PKGS}):
        res = hooks.get_requires_for_build_sdist({})
    assert res == ["frog"]


def test_prepare_metadata_for_build_wheel():
    hooks = get_hooks("pkg1")
    with TemporaryDirectory() as metadatadir:
        with modified_env({"PYTHONPATH": BUILDSYS_PKGS}):
            hooks.prepare_metadata_for_build_wheel(metadatadir, {})

        assert_isfile(pjoin(metadatadir, "pkg1-0.5.dist-info", "METADATA"))


def test_prepare_metadata_for_build_editable():
    hooks = get_hooks("pkg1")
    with TemporaryDirectory() as metadatadir:
        with modified_env({"PYTHONPATH": BUILDSYS_PKGS}):
            hooks.prepare_metadata_for_build_editable(metadatadir, {})

        assert_isfile(pjoin(metadatadir, "pkg1-0.5.dist-info", "METADATA"))


def test_build_wheel():
    hooks = get_hooks("pkg1")
    with TemporaryDirectory() as builddir:
        with modified_env({"PYTHONPATH": BUILDSYS_PKGS}):
            whl_file = hooks.build_wheel(builddir, {})

        assert whl_file.endswith(".whl")
        assert os.sep not in whl_file

        whl_file = pjoin(builddir, whl_file)
        assert_isfile(whl_file)
        assert zipfile.is_zipfile(whl_file)


def test_build_editable():
    hooks = get_hooks("pkg1")
    with TemporaryDirectory() as builddir:
        with modified_env({"PYTHONPATH": BUILDSYS_PKGS}):
            whl_file = hooks.build_editable(builddir, {})

        assert whl_file.endswith(".whl")
        assert os.sep not in whl_file

        whl_file = pjoin(builddir, whl_file)
        assert_isfile(whl_file)
        assert zipfile.is_zipfile(whl_file)


def test_build_wheel_relpath():
    hooks = get_hooks("pkg1")
    with TemporaryWorkingDirectory() as builddir:
        with modified_env({"PYTHONPATH": BUILDSYS_PKGS}):
            whl_file = hooks.build_wheel(".", {})

        assert whl_file.endswith(".whl")
        assert os.sep not in whl_file

        whl_file = pjoin(builddir, whl_file)
        assert_isfile(whl_file)
        assert zipfile.is_zipfile(whl_file)


def test_build_sdist():
    hooks = get_hooks("pkg1")
    with TemporaryDirectory() as sdistdir:
        with modified_env({"PYTHONPATH": BUILDSYS_PKGS}):
            sdist = hooks.build_sdist(sdistdir, {})

        assert sdist.endswith(".tar.gz")
        assert os.sep not in sdist

        sdist = pjoin(sdistdir, sdist)
        assert_isfile(sdist)
        assert tarfile.is_tarfile(sdist)

        with tarfile.open(sdist) as tf:
            contents = tf.getnames()
        assert "pkg1-0.5/pyproject.toml" in contents


def test_build_sdist_unsupported():
    hooks = get_hooks("pkg1")
    with TemporaryDirectory() as sdistdir:
        with modified_env({"PYTHONPATH": BUILDSYS_PKGS}):
            with pytest.raises(UnsupportedOperation):
                hooks.build_sdist(sdistdir, {"test_unsupported": True})


def test_runner_replaced_on_exception(monkeypatch):
    monkeypatch.setenv("PYTHONPATH", BUILDSYS_PKGS)

    runner = Mock(wraps=default_subprocess_runner)
    hooks = get_hooks("pkg1", runner=runner)

    hooks.get_requires_for_build_wheel()
    runner.assert_called_once()
    runner.reset_mock()

    runner2 = Mock(wraps=default_subprocess_runner)
    try:
        with hooks.subprocess_runner(runner2):
            hooks.get_requires_for_build_wheel()
            runner2.assert_called_once()
            runner2.reset_mock()
            raise RuntimeError()
    except RuntimeError:
        pass

    hooks.get_requires_for_build_wheel()
    runner.assert_called_once()


def test_custom_python_executable(monkeypatch, tmpdir):
    monkeypatch.setenv("PYTHONPATH", BUILDSYS_PKGS)

    runner = Mock(autospec=default_subprocess_runner)
    hooks = get_hooks("pkg1", runner=runner, python_executable="some-python")

    with hooks.subprocess_runner(runner):
        with pytest.raises(FileNotFoundError):
            # output.json is missing because we didn't actually run the hook
            hooks.get_requires_for_build_wheel()
        runner.assert_called_once()
        assert runner.call_args[0][0][0] == "some-python"


def test_issue_104():
    hooks = get_hooks("test-for-issue-104")
    with TemporaryDirectory() as outdir:
        with modified_env(
            {
                "PYTHONPATH": BUILDSYS_PKGS,
                "PEP517_ISSUE104_OUTDIR": outdir,
            }
        ):
            hooks.get_requires_for_build_wheel({})
        with open(pjoin(outdir, "out.json")) as f:
            children = json.load(f)
    assert set(children) <= {
        "__init__.py",
        "__init__.pyc",
        "_in_process.py",
        "_in_process.pyc",
        "__pycache__",
    }


def test_setup_py():
    hooks = get_hooks("setup-py")
    with modified_env({"PYTHONPATH": BUILDSYS_PKGS}):
        res = hooks.get_requires_for_build_wheel({})
    # Some versions of setuptools list setuptools itself here
    res = [x for x in res if x != "setuptools"]
    assert res == ["wheel"]


@pytest.mark.parametrize(
    ("pkg", "expected"),
    [
        ("pkg1", ["build_editable"]),
        ("pkg2", []),
        ("pkg3", ["build_editable"]),
    ],
)
def test__supported_features(pkg, expected):
    hooks = get_hooks(pkg)
    with modified_env({"PYTHONPATH": BUILDSYS_PKGS}):
        res = hooks._supported_features()
    assert res == expected
