# Copyright 2026 Google LLC
#
# Licensed 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
#
#      https://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.

import asyncio
from unittest import mock

import pytest

from bumble import hci
from bumble.transport import usb


def test_safe_call_soon_ignores_closed_loop():
    closed_loop = asyncio.new_event_loop()
    closed_loop.close()
    callback = mock.Mock()

    usb._safe_call_soon(closed_loop, callback)

    callback.assert_not_called()


@pytest.mark.asyncio
async def test_safe_call_soon_schedules_callback_on_open_loop():
    called = asyncio.Event()
    args = []

    def callback(value):
        args.append(value)
        called.set()

    usb._safe_call_soon(asyncio.get_running_loop(), callback, 'scheduled')

    await asyncio.wait_for(called.wait(), timeout=1.0)
    assert args == ['scheduled']


@pytest.mark.asyncio
async def test_usb_packet_sink_iso_routing():
    # Mock usb1 device and endpoints
    mock_device = mock.Mock()
    mock_bulk_out = mock.Mock()
    mock_bulk_out.getAddress.return_value = 0x02

    # Scenario 1: Isochronous endpoints are not enabled (isochronous_out is None)
    mock_transfer = mock.Mock()
    mock_device.getTransfer.return_value = mock_transfer

    sink = usb.UsbPacketSink(mock_device, mock_bulk_out, isochronous_out=None)
    sink.start()

    # Send HCI_ISO_DATA_PACKET
    iso_packet = bytes([hci.HCI_ISO_DATA_PACKET, 0x01, 0x02, 0x03])
    sink.on_packet(iso_packet)

    # Yield control to let the queue processor run
    await asyncio.sleep(0.01)

    # Verify it was sent via bulk transfer
    mock_transfer.setBulk.assert_called_once_with(
        0x02,
        bytes([0x01, 0x02, 0x03]),
        callback=sink.transfer_callback,
    )
    mock_transfer.submit.assert_called_once()

    if sink.queue_task:
        sink.queue_task.cancel()
        try:
            await sink.queue_task
        except asyncio.CancelledError:
            pass


@pytest.mark.asyncio
async def test_usb_packet_sink_iso_routing_with_iso_endpoint():
    # Mock usb1 device and endpoints
    mock_device = mock.Mock()
    mock_bulk_out = mock.Mock()
    mock_bulk_out.getAddress.return_value = 0x02
    mock_iso_out = mock.Mock()
    mock_iso_out.getMaxPacketSize.return_value = 64

    # Scenario 2: Isochronous endpoints are enabled
    mock_transfer_bulk = mock.Mock()
    mock_transfer_iso = mock.Mock()

    # getTransfer is called twice: once for bulk_or_control and once for isochronous
    mock_device.getTransfer.side_effect = [mock_transfer_bulk, mock_transfer_iso]

    sink = usb.UsbPacketSink(mock_device, mock_bulk_out, isochronous_out=mock_iso_out)
    sink.start()

    # Send HCI_ISO_DATA_PACKET
    iso_packet = bytes([hci.HCI_ISO_DATA_PACKET, 0x01, 0x02, 0x03])
    sink.on_packet(iso_packet)

    # Yield control to let the queue processor run
    await asyncio.sleep(0.01)

    # Verify it was NOT sent via bulk transfer
    mock_transfer_bulk.setBulk.assert_not_called()

    if sink.queue_task:
        sink.queue_task.cancel()
        try:
            await sink.queue_task
        except asyncio.CancelledError:
            pass
