# Copyright (c) 2018 Foundries.io
#
# SPDX-License-Identifier: Apache-2.0

import argparse
import itertools
from unittest.mock import patch

import pytest

from runners.pyocd import PyOcdBinaryRunner
from conftest import RC_BUILD_DIR, RC_GDB, RC_KERNEL_HEX, RC_KERNEL_ELF


#
# Test values to provide as constructor arguments and command line
# parameters, to verify they're respected.
#

TEST_PYOCD = 'test-pyocd'
TEST_ADDR = 0xadd
TEST_DEV_ID = 'test-dev-id'
TEST_FREQUENCY = 'test-frequency'
TEST_DAPARG = 'test-daparg'
TEST_TARGET = 'test-target'
TEST_FLASH_OPTS = ['--test-flash', 'args']
TEST_GDB_PORT = 1
TEST_TELNET_PORT = 2
TEST_TOOL_OPTS = ['test-opt-1', 'test-opt-2']

TEST_ALL_KWARGS = {
    'pyocd': TEST_PYOCD,
    'flash_addr': TEST_ADDR,
    'flash_opts': TEST_FLASH_OPTS,
    'gdb_port': TEST_GDB_PORT,
    'telnet_port': TEST_TELNET_PORT,
    'tui': False,
    'dev_id': TEST_DEV_ID,
    'frequency': TEST_FREQUENCY,
    'daparg': TEST_DAPARG,
    'tool_opt': TEST_TOOL_OPTS
}

TEST_DEF_KWARGS = {}

TEST_ALL_PARAMS = list(itertools.chain(
    ['--target', TEST_TARGET,
     '--daparg', TEST_DAPARG,
     '--pyocd', TEST_PYOCD],
    [f'--flash-opt={o}' for o in TEST_FLASH_OPTS],
    ['--gdb-port', str(TEST_GDB_PORT),
     '--telnet-port', str(TEST_TELNET_PORT),
     '--dev-id', TEST_DEV_ID,
     '--frequency', str(TEST_FREQUENCY)],
    [f'--tool-opt={o}' for o in TEST_TOOL_OPTS]))

TEST_DEF_PARAMS = ['--target', TEST_TARGET]

#
# Expected results.
#
# These record expected argument lists for system calls made by the
# pyocd runner using its check_call() and run_server_and_client()
# methods.
#
# They are shared between tests that create runners directly and
# tests that construct runners from parsed command-line arguments, to
# ensure that results are consistent.
#

FLASH_ALL_EXPECTED_CALL = ([TEST_PYOCD,
                            'flash',
                            '-e', 'sector',
                            '-a', hex(TEST_ADDR), '-da', TEST_DAPARG,
                            '-t', TEST_TARGET, '-u', TEST_DEV_ID,
                            '-f', TEST_FREQUENCY] +
                           TEST_TOOL_OPTS +
                           TEST_FLASH_OPTS +
                           [RC_KERNEL_HEX])
FLASH_DEF_EXPECTED_CALL = ['pyocd', 'flash', '-e', 'sector',
                           '-t', TEST_TARGET, RC_KERNEL_HEX]


DEBUG_ALL_EXPECTED_SERVER = [TEST_PYOCD,
                             'gdbserver',
                             '-da', TEST_DAPARG,
                             '-p', str(TEST_GDB_PORT),
                             '-T', str(TEST_TELNET_PORT),
                             '-t', TEST_TARGET,
                             '-u', TEST_DEV_ID,
                             '-f', TEST_FREQUENCY] + TEST_TOOL_OPTS
DEBUG_ALL_EXPECTED_CLIENT = [RC_GDB, RC_KERNEL_ELF,
                             '-ex', 'target remote :{}'.format(TEST_GDB_PORT),
                             '-ex', 'monitor halt',
                             '-ex', 'monitor reset',
                             '-ex', 'load']
DEBUG_DEF_EXPECTED_SERVER = ['pyocd',
                             'gdbserver',
                             '-p', '3333',
                             '-T', '4444',
                             '-t', TEST_TARGET]
DEBUG_DEF_EXPECTED_CLIENT = [RC_GDB, RC_KERNEL_ELF,
                             '-ex', 'target remote :3333',
                             '-ex', 'monitor halt',
                             '-ex', 'monitor reset',
                             '-ex', 'load']


DEBUGSERVER_ALL_EXPECTED_CALL = [TEST_PYOCD,
                                 'gdbserver',
                                 '-da', TEST_DAPARG,
                                 '-p', str(TEST_GDB_PORT),
                                 '-T', str(TEST_TELNET_PORT),
                                 '-t', TEST_TARGET,
                                 '-u', TEST_DEV_ID,
                                 '-f', TEST_FREQUENCY] + TEST_TOOL_OPTS
DEBUGSERVER_DEF_EXPECTED_CALL = ['pyocd',
                                 'gdbserver',
                                 '-p', '3333',
                                 '-T', '4444',
                                 '-t', TEST_TARGET]


#
# Fixtures
#

@pytest.fixture
def pyocd(runner_config, tmpdir):
    '''PyOcdBinaryRunner from constructor kwargs or command line parameters'''
    # This factory takes either a dict of kwargs to pass to the
    # constructor, or a list of command-line arguments to parse and
    # use with the create() method.
    def _factory(args):
        # Ensure kernel binaries exist (as empty files, so commands
        # which use them must be patched out).
        tmpdir.ensure(RC_KERNEL_HEX)
        tmpdir.ensure(RC_KERNEL_ELF)
        tmpdir.chdir()

        if isinstance(args, dict):
            return PyOcdBinaryRunner(runner_config, TEST_TARGET, **args)
        elif isinstance(args, list):
            parser = argparse.ArgumentParser(allow_abbrev=False)
            PyOcdBinaryRunner.add_parser(parser)
            arg_namespace = parser.parse_args(args)
            return PyOcdBinaryRunner.create(runner_config, arg_namespace)
    return _factory


#
# Helpers
#

def require_patch(program):
    assert program in ['pyocd', TEST_PYOCD, RC_GDB]


#
# Test cases for runners created by constructor.
#

@pytest.mark.parametrize('pyocd_args,expected', [
    (TEST_ALL_KWARGS, FLASH_ALL_EXPECTED_CALL),
    (TEST_DEF_KWARGS, FLASH_DEF_EXPECTED_CALL)
])
@patch('runners.pyocd.PyOcdBinaryRunner.check_call')
@patch('runners.core.ZephyrBinaryRunner.require', side_effect=require_patch)
def test_flash(require, cc, pyocd_args, expected, pyocd):
    pyocd(pyocd_args).run('flash')
    assert require.called
    cc.assert_called_once_with(expected)


@pytest.mark.parametrize('pyocd_args,expectedv', [
    (TEST_ALL_KWARGS, (DEBUG_ALL_EXPECTED_SERVER, DEBUG_ALL_EXPECTED_CLIENT)),
    (TEST_DEF_KWARGS, (DEBUG_DEF_EXPECTED_SERVER, DEBUG_DEF_EXPECTED_CLIENT))
])
@patch('runners.pyocd.PyOcdBinaryRunner.run_server_and_client')
@patch('runners.core.ZephyrBinaryRunner.require', side_effect=require_patch)
def test_debug(require, rsc, pyocd_args, expectedv, pyocd):
    pyocd(pyocd_args).run('debug')
    assert require.called
    rsc.assert_called_once_with(*expectedv)


@pytest.mark.parametrize('pyocd_args,expected', [
    (TEST_ALL_KWARGS, DEBUGSERVER_ALL_EXPECTED_CALL),
    (TEST_DEF_KWARGS, DEBUGSERVER_DEF_EXPECTED_CALL)
])
@patch('runners.pyocd.PyOcdBinaryRunner.check_call')
@patch('runners.core.ZephyrBinaryRunner.require', side_effect=require_patch)
def test_debugserver(require, cc, pyocd_args, expected, pyocd):
    pyocd(pyocd_args).run('debugserver')
    assert require.called
    cc.assert_called_once_with(expected)


#
# Test cases for runners created via command line arguments.
#
# (Unlike the constructor tests, these require additional patching to mock and
# verify runners.core.BuildConfiguration usage.)
#

@pytest.mark.parametrize('pyocd_args,flash_addr,expected', [
    (TEST_ALL_PARAMS, TEST_ADDR, FLASH_ALL_EXPECTED_CALL),
    (TEST_DEF_PARAMS, 0x0, FLASH_DEF_EXPECTED_CALL)
])
@patch('runners.pyocd.BuildConfiguration')
@patch('runners.pyocd.PyOcdBinaryRunner.check_call')
@patch('runners.core.ZephyrBinaryRunner.require', side_effect=require_patch)
def test_flash_args(require, cc, bc, pyocd_args, flash_addr, expected, pyocd):
    with patch.object(PyOcdBinaryRunner, 'get_flash_address',
                      return_value=flash_addr):
        pyocd(pyocd_args).run('flash')
        assert require.called
        bc.assert_called_once_with(RC_BUILD_DIR)
        cc.assert_called_once_with(expected)


@pytest.mark.parametrize('pyocd_args, expectedv', [
    (TEST_ALL_PARAMS, (DEBUG_ALL_EXPECTED_SERVER, DEBUG_ALL_EXPECTED_CLIENT)),
    (TEST_DEF_PARAMS, (DEBUG_DEF_EXPECTED_SERVER, DEBUG_DEF_EXPECTED_CLIENT)),
])
@patch('runners.pyocd.BuildConfiguration')
@patch('runners.pyocd.PyOcdBinaryRunner.run_server_and_client')
@patch('runners.core.ZephyrBinaryRunner.require', side_effect=require_patch)
def test_debug_args(require, rsc, bc, pyocd_args, expectedv, pyocd):
    pyocd(pyocd_args).run('debug')
    assert require.called
    bc.assert_called_once_with(RC_BUILD_DIR)
    rsc.assert_called_once_with(*expectedv)


@pytest.mark.parametrize('pyocd_args, expected', [
    (TEST_ALL_PARAMS, DEBUGSERVER_ALL_EXPECTED_CALL),
    (TEST_DEF_PARAMS, DEBUGSERVER_DEF_EXPECTED_CALL),
])
@patch('runners.pyocd.BuildConfiguration')
@patch('runners.pyocd.PyOcdBinaryRunner.check_call')
@patch('runners.core.ZephyrBinaryRunner.require', side_effect=require_patch)
def test_debugserver_args(require, cc, bc, pyocd_args, expected, pyocd):
    pyocd(pyocd_args).run('debugserver')
    assert require.called
    bc.assert_called_once_with(RC_BUILD_DIR)
    cc.assert_called_once_with(expected)
