# Copyright (c) 2023 Nordic Semiconductor ASA
#
# SPDX-License-Identifier: Apache-2.0

import os
import time
from pathlib import Path
from unittest import mock

import pytest

from twister_harness.device.hardware_adapter import HardwareAdapter
from twister_harness.exceptions import TwisterHarnessException
from twister_harness.twister_harness_config import DeviceConfig


@pytest.fixture(name='device')
def fixture_adapter(tmp_path) -> HardwareAdapter:
    build_dir = tmp_path / 'build_dir'
    os.mkdir(build_dir)
    device_config = DeviceConfig(
        type='hardware',
        build_dir=build_dir,
        runner='runner',
        platform='platform',
        id='p_id',
        base_timeout=5.0,
    )
    return HardwareAdapter(device_config)


@mock.patch('shutil.which', return_value=None)
def test_if_hardware_adapter_raise_exception_when_west_not_found(patched_which, device: HardwareAdapter) -> None:
    with pytest.raises(TwisterHarnessException, match='west not found'):
        device.generate_command()


@mock.patch('shutil.which', return_value='west')
def test_if_get_command_returns_proper_string_1(patched_which, device: HardwareAdapter) -> None:
    device.device_config.build_dir = Path('build')
    device.generate_command()
    assert isinstance(device.command, list)
    assert device.command == ['west', 'flash', '--skip-rebuild', '--build-dir', 'build', '--runner', 'runner']


@mock.patch('shutil.which', return_value='west')
def test_if_get_command_returns_proper_string_2(patched_which, device: HardwareAdapter) -> None:
    device.device_config.build_dir = Path('build')
    device.device_config.runner = 'pyocd'
    device.generate_command()
    assert isinstance(device.command, list)
    assert device.command == [
        'west', 'flash', '--skip-rebuild', '--build-dir', 'build', '--runner', 'pyocd', '--', '--board-id', 'p_id'
    ]


@mock.patch('shutil.which', return_value='west')
def test_if_get_command_returns_proper_string_3(patched_which, device: HardwareAdapter) -> None:
    device.device_config.build_dir = Path('build')
    device.device_config.runner = 'nrfjprog'
    device.generate_command()
    assert isinstance(device.command, list)
    assert device.command == [
        'west', 'flash', '--skip-rebuild', '--build-dir', 'build', '--runner', 'nrfjprog', '--', '--dev-id', 'p_id'
    ]


@mock.patch('shutil.which', return_value='west')
def test_if_get_command_returns_proper_string_4(patched_which, device: HardwareAdapter) -> None:
    device.device_config.build_dir = Path('build')
    device.device_config.runner = 'openocd'
    device.device_config.product = 'STM32 STLink'
    device.generate_command()
    assert isinstance(device.command, list)
    assert device.command == [
        'west', 'flash', '--skip-rebuild', '--build-dir', 'build', '--runner', 'openocd',
        '--', '--cmd-pre-init', 'hla_serial p_id'
    ]


@mock.patch('shutil.which', return_value='west')
def test_if_get_command_returns_proper_string_5(patched_which, device: HardwareAdapter) -> None:
    device.device_config.build_dir = Path('build')
    device.device_config.runner = 'openocd'
    device.device_config.product = 'EDBG CMSIS-DAP'
    device.generate_command()
    assert isinstance(device.command, list)
    assert device.command == [
        'west', 'flash', '--skip-rebuild', '--build-dir', 'build', '--runner', 'openocd',
        '--', '--cmd-pre-init', 'cmsis_dap_serial p_id'
    ]


@mock.patch('shutil.which', return_value='west')
def test_if_get_command_returns_proper_string_6(patched_which, device: HardwareAdapter) -> None:
    device.device_config.build_dir = Path('build')
    device.device_config.runner = 'jlink'
    device.generate_command()
    assert isinstance(device.command, list)
    assert device.command == [
        'west', 'flash', '--skip-rebuild', '--build-dir', 'build', '--runner', 'jlink',
        '--tool-opt=-SelectEmuBySN p_id'
    ]


@mock.patch('shutil.which', return_value='west')
def test_if_get_command_returns_proper_string_7(patched_which, device: HardwareAdapter) -> None:
    device.device_config.build_dir = Path('build')
    device.device_config.runner = 'stm32cubeprogrammer'
    device.generate_command()
    assert isinstance(device.command, list)
    assert device.command == [
        'west', 'flash', '--skip-rebuild', '--build-dir', 'build', '--runner', 'stm32cubeprogrammer',
        '--tool-opt=sn=p_id'
    ]


@mock.patch('shutil.which', return_value='west')
def test_if_get_command_returns_proper_string_8(patched_which, device: HardwareAdapter) -> None:
    device.device_config.build_dir = Path('build')
    device.device_config.runner = 'openocd'
    device.device_config.product = 'STLINK-V3'
    device.generate_command()
    assert isinstance(device.command, list)
    assert device.command == [
        'west', 'flash', '--skip-rebuild', '--build-dir', 'build',
        '--runner', 'openocd', '--', '--cmd-pre-init', 'hla_serial p_id'
    ]


@mock.patch('shutil.which', return_value='west')
def test_if_get_command_returns_proper_string_with_runner_params_1(patched_which, device: HardwareAdapter) -> None:
    device.device_config.build_dir = Path('build')
    device.device_config.runner_params = ['--runner-param1', 'runner-param2']
    device.generate_command()
    assert isinstance(device.command, list)
    assert device.command == [
        'west', 'flash', '--skip-rebuild', '--build-dir', 'build',
        '--runner', 'runner', '--', '--runner-param1', 'runner-param2'
    ]


@mock.patch('shutil.which', return_value='west')
def test_if_get_command_returns_proper_string_with_runner_params_2(patched_which, device: HardwareAdapter) -> None:
    device.device_config.build_dir = Path('build')
    device.device_config.runner = 'openocd'
    device.device_config.runner_params = [
        '--cmd-pre-init', 'adapter serial FT1LRSRD',
        '--cmd-pre-init', 'source [find interface/ftdi/jtag-lock-pick_tiny_2.cfg]',
        '--cmd-pre-init', 'transport select swd',
        '--cmd-pre-init', 'source [find target/nrf52.cfg]',
        '--cmd-pre-init', 'adapter speed 10000',
    ]
    device.device_config.product = 'JTAG-lock-pick Tiny 2'
    device.generate_command()
    assert isinstance(device.command, list)
    assert device.command == [
        'west', 'flash', '--skip-rebuild', '--build-dir', 'build',
        '--runner', 'openocd', '--',
        '--cmd-pre-init', 'adapter serial FT1LRSRD',
        '--cmd-pre-init', 'source [find interface/ftdi/jtag-lock-pick_tiny_2.cfg]',
        '--cmd-pre-init', 'transport select swd',
        '--cmd-pre-init', 'source [find target/nrf52.cfg]',
        '--cmd-pre-init', 'adapter speed 10000',
    ]


@mock.patch('shutil.which', return_value='west')
def test_if_get_command_returns_proper_string_with_west_flash_extra_args(
    patched_which, device: HardwareAdapter
) -> None:
    device.device_config.build_dir = Path('build')
    device.device_config.west_flash_extra_args = ['--board-id=foobar', '--erase']
    device.device_config.runner = 'pyocd'
    device.device_config.id = ''
    device.generate_command()
    assert isinstance(device.command, list)
    assert device.command == [
        'west', 'flash', '--skip-rebuild', '--build-dir', 'build', '--runner', 'pyocd',
        '--', '--board-id=foobar', '--erase'
    ]


def test_if_hardware_adapter_raises_exception_empty_command(device: HardwareAdapter) -> None:
    device.command = []
    exception_msg = 'Flash command is empty, please verify if it was generated properly.'
    with pytest.raises(TwisterHarnessException, match=exception_msg):
        device._flash_and_run()


@mock.patch('twister_harness.device.hardware_adapter.subprocess.Popen')
def test_device_log_correct_error_handle(patched_popen, device: HardwareAdapter, tmp_path: Path) -> None:
    popen_mock = mock.Mock()
    popen_mock.communicate.return_value = (b'flashing error', b'')
    patched_popen.return_value = popen_mock
    device.device_config.build_dir = tmp_path
    device.command = [
        'west', 'flash', '--skip-rebuild', '--build-dir', str(tmp_path),
        '--runner', 'nrfjprog', '--', '--dev-id', 'p_id'
    ]
    with pytest.raises(expected_exception=TwisterHarnessException, match='Could not flash device p_id'):
        device._flash_and_run()
    assert os.path.isfile(device.device_log_path)
    with open(device.device_log_path, 'r') as file:
        assert 'flashing error' in file.readlines()


@mock.patch('twister_harness.device.hardware_adapter.subprocess.Popen')
@mock.patch('twister_harness.device.hardware_adapter.serial.Serial')
def test_if_hardware_adapter_uses_serial_pty(
    patched_serial, patched_popen, device: HardwareAdapter, monkeypatch: pytest.MonkeyPatch
):
    device.device_config.serial_pty = 'script.py'

    popen_mock = mock.Mock()
    popen_mock.communicate.return_value = (b'output', b'error')
    patched_popen.return_value = popen_mock

    monkeypatch.setattr('twister_harness.device.hardware_adapter.pty.openpty', lambda: (123, 456))
    monkeypatch.setattr('twister_harness.device.hardware_adapter.os.ttyname', lambda x: f'/pty/ttytest/{x}')

    serial_mock = mock.Mock()
    serial_mock.port = '/pty/ttytest/456'
    patched_serial.return_value = serial_mock

    device._device_run.set()
    device.connect()
    assert device._serial_connection.port == '/pty/ttytest/456'  # type: ignore[union-attr]
    assert device._serial_pty_proc
    patched_popen.assert_called_with(
        ['script.py'],
        stdout=123,
        stdin=123,
        stderr=123
    )

    device.disconnect()
    assert not device._serial_pty_proc


def test_if_hardware_adapter_properly_send_data_to_subprocess(
    device: HardwareAdapter, shell_simulator_path: str
) -> None:
    """
    Run shell_simulator.py under serial_pty, send "zen" command and verify
    output. Flashing command is mocked by "dummy" echo command.
    """
    device.command = ['echo', 'TEST']  # only to mock flashing command
    device.device_config.serial_pty = f'python3 {shell_simulator_path}'
    device.launch()
    time.sleep(0.1)
    device.write(b'zen\n')
    time.sleep(1)
    lines = device.readlines_until(regex='Namespaces are one honking great idea')
    assert 'The Zen of Python, by Tim Peters' in lines
    device.write(b'quit\n')
