# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0

import os
from unittest.mock import patch

import pytest

from haystack.utils import ComponentDevice, Device, DeviceMap, DeviceType


def test_device_type():
    for e in DeviceType:
        assert e == DeviceType.from_str(e.value)

    with pytest.raises(ValueError, match="Unknown device type string"):
        DeviceType.from_str("tpu")


def test_device_creation():
    assert Device.cpu().type == DeviceType.CPU
    assert Device.gpu().type == DeviceType.GPU
    assert Device.mps().type == DeviceType.MPS
    assert Device.xpu().type == DeviceType.XPU
    assert Device.disk().type == DeviceType.DISK

    assert Device.from_str("cpu") == Device.cpu()
    assert Device.from_str("cuda:1") == Device.gpu(1)
    assert Device.from_str("disk") == Device.disk()
    assert Device.from_str("mps:0") == Device(DeviceType.MPS, 0)
    assert Device.from_str("xpu:0") == Device(DeviceType.XPU, 0)

    with pytest.raises(ValueError, match="Device id must be >= 0"):
        Device.gpu(-1)


def test_device_map():
    device_map = DeviceMap({"layer1": Device.cpu(), "layer2": Device.gpu(1), "layer3": Device.disk()})

    assert all(x in device_map for x in ["layer1", "layer2", "layer3"])
    assert len(device_map) == 3
    assert device_map["layer1"] == Device.cpu()
    assert device_map["layer2"] == Device.gpu(1)
    assert device_map["layer3"] == Device.disk()

    for k, d in device_map:
        assert k in ["layer1", "layer2", "layer3"]
        assert d in [Device.cpu(), Device.gpu(1), Device.disk()]

    device_map["layer1"] = Device.gpu(0)
    assert device_map["layer1"] == Device.gpu(0)

    device_map["layer2"] = Device.cpu()
    assert DeviceMap.from_hf({"layer1": 0, "layer2": "cpu", "layer3": "disk"}) == DeviceMap(
        {"layer1": Device.gpu(0), "layer2": Device.cpu(), "layer3": Device.disk()}
    )

    with pytest.raises(ValueError, match="unexpected device"):
        DeviceMap.from_hf({"layer1": 0.1})

    assert device_map.first_device == Device.gpu(0)
    assert DeviceMap({}).first_device is None


def test_component_device_empty_and_full():
    with pytest.raises(ValueError, match="neither be empty nor contain"):
        ComponentDevice().first_device

    with pytest.raises(ValueError, match="neither be empty nor contain"):
        ComponentDevice(Device.cpu(), DeviceMap({})).to_hf()


def test_component_device_single():
    single = ComponentDevice.from_single(Device.gpu(1))
    assert not single.has_multiple_devices
    assert single._single_device == Device.gpu(1)
    assert single._multiple_devices is None

    with pytest.raises(ValueError, match="disk device can only be used as a part of device maps"):
        ComponentDevice.from_single(Device.disk())

    assert single.to_torch_str() == "cuda:1"
    assert single.to_spacy() == 1
    assert single.to_hf() == "cuda:1"
    assert single.update_hf_kwargs({}, overwrite=False) == {"device": "cuda:1"}
    assert single.update_hf_kwargs({"device": 0}, overwrite=True) == {"device": "cuda:1"}
    assert single.first_device == ComponentDevice.from_single(single._single_device)


def test_component_device_multiple():
    multiple = ComponentDevice.from_multiple(
        DeviceMap({"layer1": Device.cpu(), "layer2": Device.gpu(1), "layer3": Device.disk()})
    )
    assert multiple.has_multiple_devices
    assert multiple._single_device is None
    assert multiple._multiple_devices == DeviceMap(
        {"layer1": Device.cpu(), "layer2": Device.gpu(1), "layer3": Device.disk()}
    )

    with pytest.raises(ValueError, match="Only single devices"):
        multiple.to_torch()

    with pytest.raises(ValueError, match="Only single devices"):
        multiple.to_torch_str()

    with pytest.raises(ValueError, match="Only single devices"):
        multiple.to_spacy()

    assert multiple.to_hf() == {"layer1": "cpu", "layer2": 1, "layer3": "disk"}
    assert multiple.update_hf_kwargs({}, overwrite=False) == {
        "device_map": {"layer1": "cpu", "layer2": 1, "layer3": "disk"}
    }
    assert multiple.update_hf_kwargs({"device_map": {None: None}}, overwrite=True) == {
        "device_map": {"layer1": "cpu", "layer2": 1, "layer3": "disk"}
    }
    assert multiple.first_device == ComponentDevice.from_single(Device.cpu())


@patch("torch.xpu.is_available")
@patch("torch.backends.mps.is_available")
@patch("torch.cuda.is_available")
def test_component_device_resolution(torch_cuda_is_available, torch_backends_mps_is_available, torch_xpu_is_available):
    assert ComponentDevice.resolve_device(ComponentDevice.from_single(Device.cpu()))._single_device == Device.cpu()

    torch_cuda_is_available.return_value = True
    assert ComponentDevice.resolve_device(None)._single_device == Device.gpu(0)

    torch_cuda_is_available.return_value = False
    torch_xpu_is_available.return_value = True
    torch_backends_mps_is_available.return_value = False
    assert ComponentDevice.resolve_device(None)._single_device == Device.xpu()

    torch_cuda_is_available.return_value = False
    torch_xpu_is_available.return_value = False
    torch_backends_mps_is_available.return_value = True
    assert ComponentDevice.resolve_device(None)._single_device == Device.mps()

    torch_cuda_is_available.return_value = False
    torch_xpu_is_available.return_value = False
    torch_backends_mps_is_available.return_value = False
    assert ComponentDevice.resolve_device(None)._single_device == Device.cpu()

    torch_cuda_is_available.return_value = False
    torch_xpu_is_available.return_value = False
    torch_backends_mps_is_available.return_value = True
    os.environ["HAYSTACK_MPS_ENABLED"] = "false"
    assert ComponentDevice.resolve_device(None)._single_device == Device.cpu()

    torch_cuda_is_available.return_value = False
    torch_xpu_is_available.return_value = True
    os.environ["HAYSTACK_XPU_ENABLED"] = "false"
    torch_backends_mps_is_available.return_value = False
    assert ComponentDevice.resolve_device(None)._single_device == Device.cpu()
