import os

import pytest

from fastNLP.core.utils.paddle_utils import _convert_data_device, paddle_to, paddle_move_data_to_device
from fastNLP.envs.imports import _NEED_IMPORT_PADDLE
if _NEED_IMPORT_PADDLE:
    import paddle

@pytest.mark.parametrize(
    ("user_visible_devices, cuda_visible_devices, device, correct"),
    (
        (None, None, 1, "gpu:1"),
        (None, "2,4,5,6", 2, "gpu:2"),
        (None, "3,4,5", 1, "gpu:1"),
        ("0,1,2,3,4,5,6,7", "0", "cpu", "cpu"),
        ("3,4,5,6,7", "0", "cpu", "cpu"),
        ("0,1,2,3,4,5,6,7", "3,4,5", "gpu:4", "gpu:1"),
        ("0,1,2,3,4,5,6,7", "3,4,5", "gpu:5", "gpu:2"),
        ("3,4,5,6", "3,5", 0, "gpu:0"),
        ("3,6,7,8", "6,7,8", "gpu:2", "gpu:1"),
    )
)
def test_convert_data_device(user_visible_devices, cuda_visible_devices, device, correct):
    _cuda_visible_devices = os.getenv("CUDA_VISIBLE_DEVICES")
    _user_visible_devices = os.getenv("USER_CUDA_VISIBLE_DEVICES")
    if cuda_visible_devices is not None:
        os.environ["CUDA_VISIBLE_DEVICES"] = cuda_visible_devices
    if user_visible_devices is not None:
        os.environ["USER_CUDA_VISIBLE_DEVICES"] = user_visible_devices
    res = _convert_data_device(device)
    assert res == correct

    # 还原环境变量
    if _cuda_visible_devices is None:
        os.environ.pop("CUDA_VISIBLE_DEVICES", None)
    else:
        os.environ["CUDA_VISIBLE_DEVICES"] = _cuda_visible_devices
    if _user_visible_devices is None:
        os.environ.pop("USER_CUDA_VISIBLE_DEVICES", None)
    else:
        os.environ["USER_CUDA_VISIBLE_DEVICES"] = _user_visible_devices

############################################################################
#
# 测试仅将单个paddle张量迁移到指定设备
#
############################################################################

@pytest.mark.paddle
class TestPaddleToDevice:
    def test_case(self):
        tensor = paddle.rand((4, 5))

        res = paddle_to(tensor, "gpu")
        assert res.place.is_gpu_place()
        assert res.place.gpu_device_id() == 0
        res = paddle_to(tensor, "cpu")
        assert res.place.is_cpu_place()

############################################################################
#
# 测试将参数中包含的所有paddle张量迁移到指定设备
#
############################################################################

@pytest.mark.paddle
class TestPaddleMoveDataToDevice:

    def check_gpu(self, tensor, idx):
        """
        检查张量是否在指定的设备上的工具函数
        """

        assert tensor.place.is_gpu_place()
        assert tensor.place.gpu_device_id() == idx

    def check_cpu(self, tensor):
        """
        检查张量是否在cpu上的工具函数
        """

        assert tensor.place.is_cpu_place()

    def test_tensor_transfer(self):
        """
        测试单个张量的迁移
        """

        paddle_tensor = paddle.rand((3, 4, 5)).cpu()
        res = paddle_move_data_to_device(paddle_tensor, device=None)
        self.check_cpu(res)

        res = paddle_move_data_to_device(paddle_tensor, device="gpu:0")
        self.check_gpu(res, 0)

    def test_list_transfer(self):
        """
        测试张量列表的迁移
        """

        paddle_list = [paddle.rand((6, 4, 2)) for i in range(10)]

        res = paddle_move_data_to_device(paddle_list, device="cpu")
        assert isinstance(res, list)
        for r in res:
            self.check_cpu(r)

        res = paddle_move_data_to_device(paddle_list, device="gpu:0")
        assert isinstance(res, list)
        for r in res:
            self.check_gpu(r, 0)

    def test_tensor_tuple_transfer(self):
        """
        测试张量元组的迁移
        """

        paddle_list = [paddle.rand((6, 4, 2)) for i in range(10)]
        paddle_tuple = tuple(paddle_list)

        res = paddle_move_data_to_device(paddle_tuple, device="cpu")
        assert isinstance(res, tuple)
        for r in res:
            self.check_cpu(r)

        res = paddle_move_data_to_device(paddle_tuple, device="gpu:0")
        assert isinstance(res, tuple)
        for r in res:
            self.check_gpu(r, 0)

    def test_dict_transfer(self):
        """
        测试字典结构的迁移
        """

        paddle_dict = {
            "tensor": paddle.rand((3, 4)),
            "list": [paddle.rand((6, 4, 2)) for i in range(10)],
            "dict":{
                "list": [paddle.rand((6, 4, 2)) for i in range(10)],
                "tensor": paddle.rand((3, 4))
            },
            "int": 2,
            "string": "test string"
        }

        res = paddle_move_data_to_device(paddle_dict, device="gpu:0")
        assert isinstance(res, dict)
        self.check_gpu(res["tensor"], 0)
        assert isinstance(res["list"], list)
        for t in res["list"]:
            self.check_gpu(t, 0)
        assert isinstance(res["int"], int)
        assert isinstance(res["string"], str)
        assert isinstance(res["dict"], dict)
        assert isinstance(res["dict"]["list"], list)
        for t in res["dict"]["list"]:
            self.check_gpu(t, 0)
        self.check_gpu(res["dict"]["tensor"], 0)

        res = paddle_move_data_to_device(paddle_dict, device="gpu:0")
        assert isinstance(res, dict)
        self.check_gpu(res["tensor"], 0)
        assert isinstance(res["list"], list)
        for t in res["list"]:
            self.check_gpu(t, 0)
        assert isinstance(res["int"], int)
        assert isinstance(res["string"], str)
        assert isinstance(res["dict"], dict)
        assert isinstance(res["dict"]["list"], list)
        for t in res["dict"]["list"]:
            self.check_gpu(t, 0)
        self.check_gpu(res["dict"]["tensor"], 0)

        res = paddle_move_data_to_device(paddle_dict, device="cpu")
        assert isinstance(res, dict)
        self.check_cpu(res["tensor"])
        assert isinstance(res["list"], list)
        for t in res["list"]:
            self.check_cpu(t)
        assert isinstance(res["int"], int)
        assert isinstance(res["string"], str)
        assert isinstance(res["dict"], dict)
        assert isinstance(res["dict"]["list"], list)
        for t in res["dict"]["list"]:
            self.check_cpu(t)
        self.check_cpu(res["dict"]["tensor"])
