# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.

from __future__ import annotations

import ctypes
import pickle

import numpy
import pytest
import tvm_ffi
from tvm_ffi import DLDeviceType


def test_device() -> None:
    device = tvm_ffi.Device("cuda", 0)
    assert device.dlpack_device_type() == tvm_ffi.DLDeviceType.kDLCUDA
    assert device.index == 0
    assert str(device) == "cuda:0"
    assert device.__repr__() == "device(type='cuda', index=0)"


def test_device_from_str() -> None:
    device = tvm_ffi.device("ext_dev:0")
    assert device.dlpack_device_type() == tvm_ffi.DLDeviceType.kDLExtDev
    assert device.index == 0
    assert str(device) == "ext_dev:0"
    assert device.__repr__() == "device(type='ext_dev', index=0)"


@pytest.mark.parametrize(
    "dev_str, expected_device_type, expect_device_id",
    [
        ("cpu", DLDeviceType.kDLCPU, 0),
        ("cuda", DLDeviceType.kDLCUDA, 0),
        ("cuda:0", DLDeviceType.kDLCUDA, 0),
        ("cuda:3", DLDeviceType.kDLCUDA, 3),
        ("metal:2", DLDeviceType.kDLMetal, 2),
    ],
)
def test_device_dlpack_device_type(
    dev_str: str,
    expected_device_type: DLDeviceType,
    expect_device_id: int,
) -> None:
    dev = tvm_ffi.device(dev_str)
    assert dev.dlpack_device_type() == expected_device_type
    assert dev.index == expect_device_id


@pytest.mark.parametrize(
    "dev_type, dev_id, expected_device_type, expect_device_id",
    [
        ("cpu", 0, DLDeviceType.kDLCPU, 0),
        ("cuda", 0, DLDeviceType.kDLCUDA, 0),
        (DLDeviceType.kDLCUDA, 0, DLDeviceType.kDLCUDA, 0),
        ("cuda", 3, DLDeviceType.kDLCUDA, 3),
        (DLDeviceType.kDLMetal, 2, DLDeviceType.kDLMetal, 2),
        # id from numpy
        ("cpu", numpy.int32(1), DLDeviceType.kDLCPU, 1),
        # id from torch (py dependency not ready in environment)
        # ("cpu", torch.tensor(1, dtype=torch.int32), DLDeviceType.kDLCPU, 1),
    ],
)
def test_device_with_dev_id(
    dev_type: str | DLDeviceType,
    dev_id: int,
    expected_device_type: DLDeviceType,
    expect_device_id: int,
) -> None:
    dev = tvm_ffi.device(dev_type, dev_id)
    assert dev.dlpack_device_type() == expected_device_type
    assert dev.index == expect_device_id


@pytest.mark.parametrize("dev_type, dev_id", [("cpu:0:0", None), ("cpu:?", None), ("cpu:", None)])
def test_deive_type_error(dev_type: str, dev_id: int | None) -> None:
    with pytest.raises(ValueError):
        tvm_ffi.device(dev_type, dev_id)


def test_deive_id_error() -> None:
    with pytest.raises(TypeError):
        tvm_ffi.device("cpu", "?")  # ty: ignore[invalid-argument-type]


def test_device_pickle() -> None:
    device = tvm_ffi.device("cuda", 0)
    device_pickled = pickle.loads(pickle.dumps(device))
    assert device_pickled.dlpack_device_type() == device.dlpack_device_type()
    assert device_pickled.index == device.index


def test_device_class_override() -> None:
    class MyDevice(tvm_ffi.Device):
        pass

    old_device = tvm_ffi.core._CLASS_DEVICE
    tvm_ffi.core._set_class_device(MyDevice)

    device = tvm_ffi.device("cuda", 0)
    assert isinstance(device, MyDevice)
    tvm_ffi.core._set_class_device(old_device)


def test_cuda_stream_handling() -> None:
    class MyDummyStream:
        def __init__(self, stream: int) -> None:
            self.stream = stream

        def __cuda_stream__(self) -> tuple[str, int]:
            return ("cuda", self.stream)

    stream = MyDummyStream(1)
    echo = tvm_ffi.get_global_func("testing.echo")
    y = echo(stream)
    assert isinstance(y, ctypes.c_void_p)
    assert y.value == 1
