#
# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
import pathlib
import re

from unittest import mock

import pytest

from mbed_tools.targets import Board

from tests.devices.factories import CandidateDeviceFactory
from mbed_tools.devices.device import Device
from mbed_tools.devices._internal.exceptions import NoBoardForCandidate, ResolveBoardError

from mbed_tools.devices.devices import (
    get_connected_devices,
    find_connected_device,
    find_all_connected_devices,
)
from mbed_tools.devices.exceptions import DeviceLookupFailed, NoDevicesFound


@mock.patch("mbed_tools.devices.devices.detect_candidate_devices")
@mock.patch("mbed_tools.devices.device.resolve_board")
class TestGetConnectedDevices:
    @mock.patch("mbed_tools.devices.device.read_device_files")
    def test_builds_devices_from_candidates(self, read_device_files, resolve_board, detect_candidate_devices):
        candidate = CandidateDeviceFactory()
        detect_candidate_devices.return_value = [candidate]
        iface_details = {"Version": "0222"}
        read_device_files.return_value = mock.Mock(interface_details=iface_details)

        connected_devices = get_connected_devices()
        assert connected_devices.identified_devices == [
            Device(
                serial_port=candidate.serial_port,
                serial_number=candidate.serial_number,
                mount_points=candidate.mount_points,
                mbed_board=resolve_board.return_value,
                mbed_enabled=True,
                interface_version=iface_details["Version"],
            )
        ]
        assert not connected_devices.unidentified_devices

    @mock.patch.object(Board, "from_offline_board_entry")
    def test_skips_candidates_without_a_board(self, board, resolve_board, detect_candidate_devices):
        candidate = CandidateDeviceFactory()
        resolve_board.side_effect = NoBoardForCandidate
        detect_candidate_devices.return_value = [candidate]
        board.return_value = None

        connected_devices = get_connected_devices()
        assert connected_devices.identified_devices == []
        assert connected_devices.unidentified_devices == [
            Device(
                serial_port=candidate.serial_port,
                serial_number=candidate.serial_number,
                mount_points=candidate.mount_points,
                mbed_board=None,
            )
        ]

    @mock.patch("mbed_tools.devices.device.read_device_files")
    def test_raises_when_resolve_board_fails(self, read_device_files, resolve_board, detect_candidate_devices):
        candidate = CandidateDeviceFactory()
        resolve_board.side_effect = ResolveBoardError
        detect_candidate_devices.return_value = [candidate]

        with pytest.raises(DeviceLookupFailed, match="candidate"):
            get_connected_devices()


@mock.patch("mbed_tools.devices.devices.find_all_connected_devices")
class TestFindConnectedDevice:
    def test_finds_device_with_matching_name(self, mock_find_connected_devices):
        target_name = "K64F"
        mock_find_connected_devices.return_value = [
            mock.Mock(mbed_board=mock.Mock(board_type=target_name, spec=True), serial_number="123", spec=True)
        ]

        dev = find_connected_device(target_name)

        assert target_name == dev.mbed_board.board_type

    def test_finds_device_with_matching_name_identifier(self, mock_find_connected_devices):
        target_name = "K64F"
        target_identifier = 1
        mock_find_connected_devices.return_value = [
            mock.Mock(mbed_board=mock.Mock(board_type=target_name, spec=True), serial_number="123", spec=True),
            mock.Mock(mbed_board=mock.Mock(board_type=target_name, spec=True), serial_number="456", spec=True),
        ]

        dev = find_connected_device(target_name, target_identifier)

        assert dev.serial_number == "456"

    def test_raises_when_multiple_matching_name_no_identifier(self, mock_find_connected_devices):
        target_name = "K64F"
        mock_find_connected_devices.return_value = [
            mock.Mock(
                serial_port="tty.0",
                mount_points=[pathlib.Path("/board")],
                mbed_board=mock.Mock(board_type=target_name, spec=True),
                serial_number="456",
                spec=True,
            ),
            mock.Mock(
                serial_port="tty.1",
                mount_points=[pathlib.Path("/board2")],
                mbed_board=mock.Mock(board_type=target_name, spec=True),
                serial_number="123",
                spec=True,
            ),
        ]

        with pytest.raises(DeviceLookupFailed, match="Multiple"):
            find_connected_device("K64F", None)

    def test_raises_when_identifier_out_of_bounds(self, mock_find_connected_devices):
        target_name = "K64F"
        mock_find_connected_devices.return_value = [
            mock.Mock(
                serial_port="tty.0",
                mount_points=[pathlib.Path("/board")],
                mbed_board=mock.Mock(board_type=target_name, spec=True),
                serial_number="456",
                spec=True,
            ),
            mock.Mock(
                serial_port="tty.1",
                mount_points=[pathlib.Path("/board2")],
                mbed_board=mock.Mock(board_type=target_name, spec=True),
                serial_number="123",
                spec=True,
            ),
        ]

        with pytest.raises(DeviceLookupFailed, match="valid"):
            find_connected_device("K64F", 2)


@mock.patch("mbed_tools.devices.devices.get_connected_devices")
class TestFindAllConnectedDevices:
    def test_finds_all_devices_with_matching_name(self, mock_get_connected_devices):
        target_name = "K64F"
        mock_get_connected_devices.return_value = mock.Mock(
            identified_devices=[
                mock.Mock(mbed_board=mock.Mock(board_type=target_name, spec=True), serial_number="456", spec=True),
                mock.Mock(mbed_board=mock.Mock(board_type=target_name, spec=True), serial_number="123", spec=True),
                mock.Mock(mbed_board=mock.Mock(board_type="DISCO", spec=True), serial_number="345", spec=True),
            ],
            spec=True,
        )

        devices = find_all_connected_devices(target_name)

        assert len(devices) == 2
        assert devices[0].serial_number == "123"
        assert devices[1].serial_number == "456"

    def test_raises_when_no_mbed_enabled_devices_found(self, mock_get_connected_devices):
        mock_get_connected_devices.return_value = mock.Mock(identified_devices=[], spec=True)

        with pytest.raises(NoDevicesFound):
            find_all_connected_devices("K64F")

    def test_raises_when_device_matching_target_name_not_found(self, mock_get_connected_devices):
        target_name = "K64F"
        connected_target_name = "DISCO"
        connected_target_serial_port = "tty.BLEH"
        connected_target_mount_point = [pathlib.Path("/dap")]
        mock_get_connected_devices.return_value = mock.Mock(
            identified_devices=[
                mock.Mock(
                    serial_port=connected_target_serial_port,
                    mount_points=connected_target_mount_point,
                    mbed_board=mock.Mock(board_type=connected_target_name, spec=True),
                    spec=True,
                )
            ],
            spec=True,
        )

        with pytest.raises(
            DeviceLookupFailed,
            match=(
                f".*(target: {re.escape(connected_target_name)}).*(port: {re.escape(connected_target_serial_port)}).*"
                f"(mount point.*: {re.escape(str(connected_target_mount_point))})"
            ),
        ):
            find_all_connected_devices(target_name)
