import collections
import os
import subprocess
import sys

import pytest


# Enable `testdir` fixture to test `cupy.testing`.
# `pytest_plugins` cannot be locally configured. See also
# https://docs.pytest.org/en/stable/deprecations.html#pytest-plugins-in-non-top-level-conftest-files
pytest_plugins = ['pytester']


def _is_pip_installed():
    try:
        import pip  # NOQA
        return True
    except ImportError:
        return False


def _is_in_ci():
    ci_name = os.environ.get('CUPY_CI', '')
    return ci_name != ''


def pytest_configure(config):
    # Print installed packages
    if _is_in_ci() and _is_pip_installed():
        print("***** Installed packages *****", flush=True)
        subprocess.check_call([sys.executable, '-m', 'pip', 'freeze', '--all'])
    if config.pluginmanager.hasplugin("xdist"):
        config.pluginmanager.register(DeferPlugin())


# https://docs.pytest.org/en/latest/how-to/writing_hook_functions.html#optionally-using-hooks-from-3rd-party-plugins
class DeferPlugin:
    """Simple plugin to defer pytest-xdist hook functions."""

    # Edit the environment variable `CUDA_VISIBLE_DEVICES` for each session.
    # Cannot use `pytest_configure_node` nor `pytest_testnodeready` hook,
    # because they are called in the `master` node (process).
    # See also https://github.com/pytest-dev/pytest-xdist/issues/179.
    @pytest.fixture(autouse=True, scope='session')
    def _rotate_cuda_visible_devices(self, worker_id):
        if worker_id == 'master':
            # `worker_id` can be `master` if `pytest-xdist` is installed and
            # run without `-n` option.
            return

        n_gpu = os.environ.get('CUPY_TEST_GPU_LIMIT')
        if n_gpu is None:
            print('Tip: when using pytest-xdist, you can automatically rotate'
                  ' CUDA_VISIBLE_DEVICES for each test worker by setting'
                  ' CUPY_TEST_GPU_LIMIT environment variable.')
            return
        n_gpu = int(n_gpu)

        assert worker_id.startswith('gw')
        w = int(worker_id[2:])

        devices = os.environ.get('CUDA_VISIBLE_DEVICES')
        if devices is None:
            devices = [str(k) for k in range(n_gpu)]
        else:
            devices = devices.split(',')[:n_gpu]
        devices = collections.deque(devices)
        devices.rotate(w)
        devices = ','.join(devices)
        os.environ['CUDA_VISIBLE_DEVICES'] = devices
        # With PyTest's default, the print will be shown as
        # "--- Captured stdout setup ---" on failure.
        print(f'CUDA_VISIBLE_DEVICES={devices}')


if int(os.environ.get('CUPY_ENABLE_UMP', 0)) != 0:
    # Make sure malloc is used in a stream-ordered fashion
    import cupy as cp
    cp.cuda.set_allocator(cp.cuda.MemoryPool(
        cp.cuda.memory.malloc_system).malloc)

    import cupy._core.numpy_allocator as ac
    import numpy_allocator
    import ctypes
    lib = ctypes.CDLL(ac.__file__)

    class my_allocator(metaclass=numpy_allocator.type):
        _calloc_ = ctypes.addressof(lib._calloc)
        _malloc_ = ctypes.addressof(lib._malloc)
        _realloc_ = ctypes.addressof(lib._realloc)
        _free_ = ctypes.addressof(lib._free)
    my_allocator.__enter__()
