# Copyright 2021-2023 H2020 TeraFlow (https://www.teraflow-h2020.eu/)
#
# 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
#
#      http://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 calendar, copy, dateutil.parser, grpc, json, logging, operator, os, pytest, queue, time
from datetime import datetime, timezone
from typing import Tuple
from common.orm.Database import Database
from common.orm.Factory import get_database_backend, BackendEnum as DatabaseBackendEnum
from common.message_broker.Factory import get_messagebroker_backend, BackendEnum as MessageBrokerBackendEnum
from common.message_broker.MessageBroker import MessageBroker
from common.tools.grpc.Tools import grpc_message_to_json_string
from common.tools.object_factory.EndPoint import json_endpoint, json_endpoint_id
from context.Config import (
    GRPC_SERVICE_PORT as CONTEXT_GRPC_SERVICE_PORT, GRPC_MAX_WORKERS as CONTEXT_GRPC_MAX_WORKERS,
    GRPC_GRACE_PERIOD as CONTEXT_GRPC_GRACE_PERIOD)
from context.client.ContextClient import ContextClient
from context.proto.context_pb2 import DeviceId, DeviceOperationalStatusEnum
from context.service.grpc_server.ContextService import ContextService
from device.Config import (
    GRPC_SERVICE_PORT as DEVICE_GRPC_SERVICE_PORT, GRPC_MAX_WORKERS as DEVICE_GRPC_MAX_WORKERS,
    GRPC_GRACE_PERIOD as DEVICE_GRPC_GRACE_PERIOD)
from device.client.DeviceClient import DeviceClient
from device.proto.context_pb2 import ConfigActionEnum, Context, Device, Topology
from device.proto.device_pb2 import MonitoringSettings
from device.proto.kpi_sample_types_pb2 import KpiSampleType
from device.service.DeviceService import DeviceService
from device.service.driver_api._Driver import _Driver
from device.service.driver_api.DriverFactory import DriverFactory
from device.service.driver_api.DriverInstanceCache import DriverInstanceCache
from device.service.drivers import DRIVERS
from device.tests.MockMonitoringService import MockMonitoringService
from monitoring.Config import (
    GRPC_SERVICE_PORT as MONITORING_GRPC_SERVICE_PORT, GRPC_MAX_WORKERS as MONITORING_GRPC_MAX_WORKERS,
    GRPC_GRACE_PERIOD as MONITORING_GRPC_GRACE_PERIOD)
from monitoring.client.monitoring_client import MonitoringClient
from .CommonObjects import CONTEXT, TOPOLOGY

from .Device_Emulated import (
    DEVICE_EMU, DEVICE_EMU_CONFIG_ADDRESSES, DEVICE_EMU_CONFIG_ENDPOINTS, DEVICE_EMU_CONNECT_RULES,
    DEVICE_EMU_DECONFIG_ADDRESSES, DEVICE_EMU_DECONFIG_ENDPOINTS, DEVICE_EMU_EP_DESCS, DEVICE_EMU_ENDPOINTS_COOKED,
    DEVICE_EMU_ID, DEVICE_EMU_RECONFIG_ADDRESSES, DEVICE_EMU_UUID)
ENABLE_EMULATED = True

try:
    from .Device_OpenConfig_Infinera1 import(
    #from .Device_OpenConfig_Infinera2 import(
        DEVICE_OC, DEVICE_OC_CONFIG_RULES, DEVICE_OC_DECONFIG_RULES, DEVICE_OC_CONNECT_RULES, DEVICE_OC_ID,
        DEVICE_OC_UUID)
    ENABLE_OPENCONFIG = True
except ImportError:
    ENABLE_OPENCONFIG = False

try:
    from .Device_Transport_Api_CTTC import (
        DEVICE_TAPI, DEVICE_TAPI_CONNECT_RULES, DEVICE_TAPI_UUID, DEVICE_TAPI_ID, DEVICE_TAPI_CONFIG_RULES,
        DEVICE_TAPI_DECONFIG_RULES)
    ENABLE_TAPI = True
except ImportError:
    ENABLE_TAPI = False

from .mock_p4runtime_service import MockP4RuntimeService
try:
    from .device_p4 import(
        DEVICE_P4, DEVICE_P4_ID, DEVICE_P4_UUID, DEVICE_P4_NAME, DEVICE_P4_ADDRESS, DEVICE_P4_PORT, DEVICE_P4_WORKERS,
        DEVICE_P4_GRACE_PERIOD, DEVICE_P4_CONNECT_RULES, DEVICE_P4_CONFIG_RULES)
    ENABLE_P4 = True
except ImportError:
    ENABLE_P4 = False

#ENABLE_EMULATED   = False # set to False to disable tests of Emulated devices
#ENABLE_OPENCONFIG = False # set to False to disable tests of OpenConfig devices
#ENABLE_TAPI       = False # set to False to disable tests of TAPI devices
ENABLE_P4         = False # set to False to disable tests of P4 devices (P4 device not available in GitLab)

ENABLE_OPENCONFIG_CONFIGURE   = True
ENABLE_OPENCONFIG_MONITOR     = True
ENABLE_OPENCONFIG_DECONFIGURE = True


logging.getLogger('apscheduler.executors.default').setLevel(logging.WARNING)
logging.getLogger('apscheduler.scheduler').setLevel(logging.WARNING)
logging.getLogger('monitoring-client').setLevel(logging.WARNING)

LOGGER = logging.getLogger(__name__)
LOGGER.setLevel(logging.DEBUG)

CONTEXT_GRPC_SERVICE_PORT = 10000 + CONTEXT_GRPC_SERVICE_PORT # avoid privileged ports
DEVICE_GRPC_SERVICE_PORT = 10000 + DEVICE_GRPC_SERVICE_PORT # avoid privileged ports
MONITORING_GRPC_SERVICE_PORT = 10000 + MONITORING_GRPC_SERVICE_PORT # avoid privileged ports

DEFAULT_REDIS_SERVICE_HOST = '127.0.0.1'
DEFAULT_REDIS_SERVICE_PORT = 6379
DEFAULT_REDIS_DATABASE_ID  = 0

REDIS_CONFIG = {
    'REDIS_SERVICE_HOST': os.environ.get('REDIS_SERVICE_HOST', DEFAULT_REDIS_SERVICE_HOST),
    'REDIS_SERVICE_PORT': os.environ.get('REDIS_SERVICE_PORT', DEFAULT_REDIS_SERVICE_PORT),
    'REDIS_DATABASE_ID' : os.environ.get('REDIS_DATABASE_ID',  DEFAULT_REDIS_DATABASE_ID ),
}

SCENARIOS = [
    ('all_inmemory', DatabaseBackendEnum.INMEMORY, {},           MessageBrokerBackendEnum.INMEMORY, {}          ),
    #('all_redis',    DatabaseBackendEnum.REDIS,    REDIS_CONFIG, MessageBrokerBackendEnum.REDIS,    REDIS_CONFIG),
]

@pytest.fixture(scope='session', ids=[str(scenario[0]) for scenario in SCENARIOS], params=SCENARIOS)
def context_db_mb(request) -> Tuple[Database, MessageBroker]:
    name,db_backend,db_settings,mb_backend,mb_settings = request.param
    msg = 'Running scenario {:s} db_backend={:s}, db_settings={:s}, mb_backend={:s}, mb_settings={:s}...'
    LOGGER.info(msg.format(str(name), str(db_backend.value), str(db_settings), str(mb_backend.value), str(mb_settings)))
    _database = Database(get_database_backend(backend=db_backend, **db_settings))
    _message_broker = MessageBroker(get_messagebroker_backend(backend=mb_backend, **mb_settings))
    yield _database, _message_broker
    _message_broker.terminate()

@pytest.fixture(scope='session')
def context_service(context_db_mb : Tuple[Database, MessageBroker]): # pylint: disable=redefined-outer-name
    _service = ContextService(
        context_db_mb[0], context_db_mb[1], port=CONTEXT_GRPC_SERVICE_PORT, max_workers=CONTEXT_GRPC_MAX_WORKERS,
        grace_period=CONTEXT_GRPC_GRACE_PERIOD)
    _service.start()
    yield _service
    _service.stop()

@pytest.fixture(scope='session')
def context_client(context_service : ContextService): # pylint: disable=redefined-outer-name
    _client = ContextClient(address='127.0.0.1', port=CONTEXT_GRPC_SERVICE_PORT)
    yield _client
    _client.close()

@pytest.fixture(scope='session')
def monitoring_service():
    _service = MockMonitoringService(port=MONITORING_GRPC_SERVICE_PORT, max_workers=MONITORING_GRPC_MAX_WORKERS,
        grace_period=MONITORING_GRPC_GRACE_PERIOD)
    _service.start()
    yield _service
    _service.stop()

@pytest.fixture(scope='session')
def monitoring_client(monitoring_service : MockMonitoringService): # pylint: disable=redefined-outer-name
    _client = MonitoringClient(server='127.0.0.1', port=MONITORING_GRPC_SERVICE_PORT)
    #yield _client
    #_client.close()
    return _client

@pytest.fixture(scope='session')
def device_service(
    context_client : ContextClient,         # pylint: disable=redefined-outer-name
    monitoring_client : MonitoringClient):  # pylint: disable=redefined-outer-name

    _driver_factory = DriverFactory(DRIVERS)
    _driver_instance_cache = DriverInstanceCache(_driver_factory)
    _service = DeviceService(
        context_client, monitoring_client, _driver_instance_cache, port=DEVICE_GRPC_SERVICE_PORT,
        max_workers=DEVICE_GRPC_MAX_WORKERS, grace_period=DEVICE_GRPC_GRACE_PERIOD)
    _service.start()
    yield _service
    _service.stop()

@pytest.fixture(scope='session')
def device_client(device_service : DeviceService): # pylint: disable=redefined-outer-name
    _client = DeviceClient(address='127.0.0.1', port=DEVICE_GRPC_SERVICE_PORT)
    yield _client
    _client.close()

@pytest.fixture(scope='session')
def p4runtime_service():
    _service = MockP4RuntimeService(
        address=DEVICE_P4_ADDRESS, port=DEVICE_P4_PORT,
        max_workers=DEVICE_P4_WORKERS,
        grace_period=DEVICE_P4_GRACE_PERIOD)
    _service.start()
    yield _service
    _service.stop()


def test_prepare_environment(
    context_client : ContextClient,     # pylint: disable=redefined-outer-name
    device_client : DeviceClient,       # pylint: disable=redefined-outer-name
    device_service : DeviceService):    # pylint: disable=redefined-outer-name

    context_client.SetContext(Context(**CONTEXT))
    context_client.SetTopology(Topology(**TOPOLOGY))


# ----- Test Device Driver Emulated --------------------------------------------
# Device Driver Emulated tests are used to validate Driver API as well as Emulated Device Driver. Note that other
# Drivers might support a different set of resource paths, and attributes/values per resource; however, they must
# implement the Driver API.

def test_device_emulated_add_error_cases(
    context_client : ContextClient,     # pylint: disable=redefined-outer-name
    device_client : DeviceClient,       # pylint: disable=redefined-outer-name
    device_service : DeviceService):    # pylint: disable=redefined-outer-name

    if not ENABLE_EMULATED: pytest.skip('Skipping test: No Emulated device has been configured')

    with pytest.raises(grpc.RpcError) as e:
        DEVICE_EMU_WITH_ENDPOINTS = copy.deepcopy(DEVICE_EMU)
        DEVICE_EMU_WITH_ENDPOINTS['device_endpoints'].append(json_endpoint(DEVICE_EMU_ID, 'ep-id', 'ep-type'))
        device_client.AddDevice(Device(**DEVICE_EMU_WITH_ENDPOINTS))
    assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
    msg_head = 'device.device_endpoints(['
    msg_tail = ']) is invalid; RPC method AddDevice does not accept Endpoints. '\
               'Endpoints are discovered through interrogation of the physical device.'
    except_msg = str(e.value.details())
    assert except_msg.startswith(msg_head) and except_msg.endswith(msg_tail)

    with pytest.raises(grpc.RpcError) as e:
        DEVICE_EMU_WITH_EXTRA_RULES = copy.deepcopy(DEVICE_EMU)
        DEVICE_EMU_WITH_EXTRA_RULES['device_config']['config_rules'].extend(DEVICE_EMU_CONNECT_RULES)
        DEVICE_EMU_WITH_EXTRA_RULES['device_config']['config_rules'].extend(DEVICE_EMU_CONFIG_ENDPOINTS)
        device_client.AddDevice(Device(**DEVICE_EMU_WITH_EXTRA_RULES))
    assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
    msg_head = 'device.device_config.config_rules(['
    msg_tail = ']) is invalid; RPC method AddDevice only accepts connection Config Rules that should start '\
               'with "_connect/" tag. Others should be configured after adding the device.'
    except_msg = str(e.value.details())
    assert except_msg.startswith(msg_head) and except_msg.endswith(msg_tail)


def test_device_emulated_add_correct(
    context_client : ContextClient,     # pylint: disable=redefined-outer-name
    device_client : DeviceClient,       # pylint: disable=redefined-outer-name
    device_service : DeviceService):    # pylint: disable=redefined-outer-name

    if not ENABLE_EMULATED: pytest.skip('Skipping test: No Emulated device has been configured')

    DEVICE_EMU_WITH_CONNECT_RULES = copy.deepcopy(DEVICE_EMU)
    DEVICE_EMU_WITH_CONNECT_RULES['device_config']['config_rules'].extend(DEVICE_EMU_CONNECT_RULES)
    device_client.AddDevice(Device(**DEVICE_EMU_WITH_CONNECT_RULES))
    driver : _Driver = device_service.driver_instance_cache.get(DEVICE_EMU_UUID) # we know the driver exists now
    assert driver is not None


def test_device_emulated_get(
    context_client : ContextClient,     # pylint: disable=redefined-outer-name
    device_client : DeviceClient,       # pylint: disable=redefined-outer-name
    device_service : DeviceService):    # pylint: disable=redefined-outer-name

    if not ENABLE_EMULATED: pytest.skip('Skipping test: No Emulated device has been configured')

    initial_config = device_client.GetInitialConfig(DeviceId(**DEVICE_EMU_ID))
    LOGGER.info('initial_config = {:s}'.format(grpc_message_to_json_string(initial_config)))

    device_data = context_client.GetDevice(DeviceId(**DEVICE_EMU_ID))
    LOGGER.info('device_data = {:s}'.format(grpc_message_to_json_string(device_data)))


def test_device_emulated_configure(
    context_client : ContextClient,     # pylint: disable=redefined-outer-name
    device_client : DeviceClient,       # pylint: disable=redefined-outer-name
    device_service : DeviceService):    # pylint: disable=redefined-outer-name

    if not ENABLE_EMULATED: pytest.skip('Skipping test: No Emulated device has been configured')

    driver : _Driver = device_service.driver_instance_cache.get(DEVICE_EMU_UUID) # we know the driver exists now
    assert driver is not None

    driver_config = sorted(driver.GetConfig(), key=operator.itemgetter(0))
    #LOGGER.info('driver_config = {:s}'.format(str(driver_config)))
    assert len(driver_config) == len(DEVICE_EMU_ENDPOINTS_COOKED)
    for endpoint_cooked in DEVICE_EMU_ENDPOINTS_COOKED:
        assert endpoint_cooked in driver_config

    DEVICE_EMU_WITH_CONFIG_RULES = copy.deepcopy(DEVICE_EMU)
    DEVICE_EMU_WITH_CONFIG_RULES['device_config']['config_rules'].extend(DEVICE_EMU_CONFIG_ENDPOINTS)
    device_client.ConfigureDevice(Device(**DEVICE_EMU_WITH_CONFIG_RULES))

    DEVICE_EMU_WITH_CONFIG_RULES = copy.deepcopy(DEVICE_EMU)
    DEVICE_EMU_WITH_CONFIG_RULES['device_config']['config_rules'].extend(DEVICE_EMU_CONFIG_ADDRESSES)
    device_client.ConfigureDevice(Device(**DEVICE_EMU_WITH_CONFIG_RULES))

    DEVICE_EMU_WITH_OPERATIONAL_STATUS = copy.deepcopy(DEVICE_EMU)
    DEVICE_EMU_WITH_OPERATIONAL_STATUS['device_operational_status'] = \
        DeviceOperationalStatusEnum.DEVICEOPERATIONALSTATUS_ENABLED
    device_client.ConfigureDevice(Device(**DEVICE_EMU_WITH_OPERATIONAL_STATUS))

    driver_config = sorted(driver.GetConfig(), key=operator.itemgetter(0))
    #LOGGER.info('driver_config = {:s}'.format(str(driver_config)))
    assert len(driver_config) == len(DEVICE_EMU_ENDPOINTS_COOKED) + len(DEVICE_EMU_CONFIG_ADDRESSES)
    for endpoint_cooked in DEVICE_EMU_ENDPOINTS_COOKED:
        endpoint_cooked = copy.deepcopy(endpoint_cooked)
        endpoint_cooked[1]['enabled'] = True
        assert endpoint_cooked in driver_config
    for config_rule in DEVICE_EMU_CONFIG_ADDRESSES:
        assert (config_rule['resource_key'], json.loads(config_rule['resource_value'])) in driver_config

    device_data = context_client.GetDevice(DeviceId(**DEVICE_EMU_ID))
    assert device_data.device_operational_status == DeviceOperationalStatusEnum.DEVICEOPERATIONALSTATUS_ENABLED

    config_rules = [
        (ConfigActionEnum.Name(config_rule.action), config_rule.resource_key, config_rule.resource_value)
        for config_rule in device_data.device_config.config_rules
    ]
    #LOGGER.info('device_data.device_config.config_rules = \n{:s}'.format(
    #    '\n'.join(['{:s} {:s} = {:s}'.format(*config_rule) for config_rule in config_rules])))
    RESULTING_CONFIG_ENDPOINTS = {cr['resource_key']:cr for cr in copy.deepcopy(DEVICE_EMU_CONFIG_ENDPOINTS)}
    for endpoint_cooked in DEVICE_EMU_ENDPOINTS_COOKED:
        values = json.loads(RESULTING_CONFIG_ENDPOINTS[endpoint_cooked[0]]['resource_value'])
        values.update(endpoint_cooked[1])
        RESULTING_CONFIG_ENDPOINTS[endpoint_cooked[0]]['resource_value'] = json.dumps(values, sort_keys=True)
    for config_rule in RESULTING_CONFIG_ENDPOINTS.values():
        config_rule = (
            ConfigActionEnum.Name(config_rule['action']), config_rule['resource_key'],
            json.loads(json.dumps(config_rule['resource_value'])))
        assert config_rule in config_rules
    for config_rule in DEVICE_EMU_CONFIG_ADDRESSES:
        config_rule = (
            ConfigActionEnum.Name(config_rule['action']), config_rule['resource_key'],
            json.loads(json.dumps(config_rule['resource_value'])))
        assert config_rule in config_rules

    # Try to reconfigure...

    DEVICE_EMU_WITH_RECONFIG_RULES = copy.deepcopy(DEVICE_EMU)
    DEVICE_EMU_WITH_RECONFIG_RULES['device_operational_status'] = \
        DeviceOperationalStatusEnum.DEVICEOPERATIONALSTATUS_ENABLED
    DEVICE_EMU_WITH_RECONFIG_RULES['device_config']['config_rules'].extend(DEVICE_EMU_RECONFIG_ADDRESSES)
    device_client.ConfigureDevice(Device(**DEVICE_EMU_WITH_RECONFIG_RULES))

    RESULTING_CONFIG_RULES = {cr['resource_key']:cr for cr in copy.deepcopy(DEVICE_EMU_CONFIG_ENDPOINTS)}
    for endpoint_cooked in DEVICE_EMU_ENDPOINTS_COOKED:
        values = json.loads(RESULTING_CONFIG_RULES[endpoint_cooked[0]]['resource_value'])
        values.update(endpoint_cooked[1])
        RESULTING_CONFIG_RULES[endpoint_cooked[0]]['resource_value'] = json.dumps(values, sort_keys=True)
    RESULTING_CONFIG_RULES.update({cr['resource_key']:cr for cr in copy.deepcopy(DEVICE_EMU_CONFIG_ADDRESSES)})
    for reconfig_rule in DEVICE_EMU_RECONFIG_ADDRESSES:
        if reconfig_rule['action'] == ConfigActionEnum.CONFIGACTION_DELETE:
            RESULTING_CONFIG_RULES.pop(reconfig_rule['resource_key'], None)
        else:
            RESULTING_CONFIG_RULES[reconfig_rule['resource_key']] = reconfig_rule
    RESULTING_CONFIG_RULES = RESULTING_CONFIG_RULES.values()
    #LOGGER.info('RESULTING_CONFIG_RULES = {:s}'.format(str(RESULTING_CONFIG_RULES)))

    driver_config = sorted(driver.GetConfig(), key=operator.itemgetter(0))
    driver_config = json.loads(json.dumps(driver_config)) # prevent integer keys to fail matching with string keys
    #LOGGER.info('driver_config = {:s}'.format(str(driver_config)))
    assert len(driver_config) == len(RESULTING_CONFIG_RULES)
    for config_rule in RESULTING_CONFIG_RULES:
        resource = [config_rule['resource_key'], json.loads(config_rule['resource_value'])]
        assert resource in driver_config

    device_data = context_client.GetDevice(DeviceId(**DEVICE_EMU_ID))
    config_rules = [
        (ConfigActionEnum.Name(config_rule.action), config_rule.resource_key, config_rule.resource_value)
        for config_rule in device_data.device_config.config_rules
    ]
    #LOGGER.info('device_data.device_config.config_rules = \n{:s}'.format(
    #    '\n'.join(['{:s} {:s} = {:s}'.format(*config_rule) for config_rule in config_rules])))
    for config_rule in RESULTING_CONFIG_RULES:
        config_rule = (
            ConfigActionEnum.Name(config_rule['action']), config_rule['resource_key'], config_rule['resource_value'])
        assert config_rule in config_rules


def test_device_emulated_monitor(
    context_client : ContextClient,                 # pylint: disable=redefined-outer-name
    device_client : DeviceClient,                   # pylint: disable=redefined-outer-name
    device_service : DeviceService,                 # pylint: disable=redefined-outer-name
    monitoring_service : MockMonitoringService):    # pylint: disable=redefined-outer-name

    if not ENABLE_EMULATED: pytest.skip('Skipping test: No Emulated device has been configured')

    device_uuid = DEVICE_EMU_UUID
    json_device_id = DEVICE_EMU_ID
    device_id = DeviceId(**json_device_id)
    device_data = context_client.GetDevice(device_id)
    #LOGGER.info('device_data = \n{:s}'.format(str(device_data)))

    driver : _Driver = device_service.driver_instance_cache.get(device_uuid) # we know the driver exists now
    assert driver is not None
    #driver_config = sorted(driver.GetConfig(), key=operator.itemgetter(0))
    #LOGGER.info('driver_config = {:s}'.format(str(driver_config)))
    #assert len(driver_config) == len(DEVICE_EMU_ENDPOINTS_COOKED) + len(DEVICE_EMU_CONFIG_ADDRESSES)

    SAMPLING_DURATION_SEC = 10.0
    SAMPLING_INTERVAL_SEC = 2.0

    MONITORING_SETTINGS_LIST = []
    KPI_UUIDS__TO__NUM_SAMPLES_RECEIVED = {}
    for endpoint in device_data.device_endpoints:
        endpoint_uuid = endpoint.endpoint_id.endpoint_uuid.uuid
        for sample_type_id in endpoint.kpi_sample_types:
            sample_type_name = str(KpiSampleType.Name(sample_type_id)).upper().replace('KPISAMPLETYPE_', '')
            kpi_uuid = '{:s}-{:s}-{:s}-kpi_uuid'.format(device_uuid, endpoint_uuid, str(sample_type_id))
            monitoring_settings = {
                'kpi_id'        : {'kpi_id': {'uuid': kpi_uuid}},
                'kpi_descriptor': {
                    'kpi_description': 'Metric {:s} for Endpoint {:s} in Device {:s}'.format(
                        sample_type_name, endpoint_uuid, device_uuid),
                    'kpi_sample_type': sample_type_id,
                    'device_id': json_device_id,
                    'endpoint_id': json_endpoint_id(json_device_id, endpoint_uuid),
                },
                'sampling_duration_s': SAMPLING_DURATION_SEC,
                'sampling_interval_s': SAMPLING_INTERVAL_SEC,
            }
            MONITORING_SETTINGS_LIST.append(monitoring_settings)
            KPI_UUIDS__TO__NUM_SAMPLES_RECEIVED[kpi_uuid] = 0

    NUM_SAMPLES_EXPECTED_PER_KPI = SAMPLING_DURATION_SEC / SAMPLING_INTERVAL_SEC
    NUM_SAMPLES_EXPECTED = len(MONITORING_SETTINGS_LIST) * NUM_SAMPLES_EXPECTED_PER_KPI

    # Start monitoring the device
    t_start_monitoring = datetime.timestamp(datetime.utcnow())
    for monitoring_settings in MONITORING_SETTINGS_LIST:
        device_client.MonitorDeviceKpi(MonitoringSettings(**monitoring_settings))

    # wait to receive the expected number of samples
    # if takes more than 1.5 times the sampling duration, assume there is an error
    time_ini = time.time()
    queue_samples : queue.Queue = monitoring_service.queue_samples
    received_samples = []
    while (len(received_samples) < NUM_SAMPLES_EXPECTED) and (time.time() - time_ini < SAMPLING_DURATION_SEC * 1.5):
        try:
            received_sample = queue_samples.get(block=True, timeout=SAMPLING_INTERVAL_SEC / NUM_SAMPLES_EXPECTED)
            #LOGGER.info('received_sample = {:s}'.format(str(received_sample)))
            received_samples.append(received_sample)
        except queue.Empty:
            continue

    t_end_monitoring = datetime.timestamp(datetime.utcnow())

    #LOGGER.info('received_samples = {:s}'.format(str(received_samples)))
    LOGGER.info('len(received_samples) = {:s}'.format(str(len(received_samples))))
    LOGGER.info('NUM_SAMPLES_EXPECTED = {:s}'.format(str(NUM_SAMPLES_EXPECTED)))
    assert len(received_samples) == NUM_SAMPLES_EXPECTED
    for received_sample in received_samples:
        kpi_uuid = received_sample.kpi_id.kpi_id.uuid
        assert kpi_uuid in KPI_UUIDS__TO__NUM_SAMPLES_RECEIVED
        assert isinstance(received_sample.timestamp, str)
        try:
            timestamp = float(received_sample.timestamp)
        except ValueError:
            dt_time = dateutil.parser.isoparse(received_sample.timestamp).replace(tzinfo=timezone.utc)
            timestamp = float(calendar.timegm(dt_time.timetuple())) + (dt_time.microsecond / 1.e6)
        assert timestamp > t_start_monitoring
        assert timestamp < t_end_monitoring
        assert received_sample.kpi_value.HasField('floatVal') or received_sample.kpi_value.HasField('intVal')
        kpi_value = getattr(received_sample.kpi_value, received_sample.kpi_value.WhichOneof('value'))
        assert isinstance(kpi_value, (float, int))
        KPI_UUIDS__TO__NUM_SAMPLES_RECEIVED[kpi_uuid] += 1

    LOGGER.info('KPI_UUIDS__TO__NUM_SAMPLES_RECEIVED = {:s}'.format(str(KPI_UUIDS__TO__NUM_SAMPLES_RECEIVED)))
    for kpi_uuid, num_samples_received in KPI_UUIDS__TO__NUM_SAMPLES_RECEIVED.items():
        assert num_samples_received == NUM_SAMPLES_EXPECTED_PER_KPI

    # Unsubscribe monitoring
    for kpi_uuid in KPI_UUIDS__TO__NUM_SAMPLES_RECEIVED.keys():
        MONITORING_SETTINGS_UNSUBSCRIBE = {
            'kpi_id'             : {'kpi_id': {'uuid': kpi_uuid}},
            'sampling_duration_s': -1, # negative value in sampling_duration_s or sampling_interval_s means unsibscribe
            'sampling_interval_s': -1, # kpi_id is mandatory to unsibscribe
        }
        device_client.MonitorDeviceKpi(MonitoringSettings(**MONITORING_SETTINGS_UNSUBSCRIBE))


def test_device_emulated_deconfigure(
    context_client : ContextClient,     # pylint: disable=redefined-outer-name
    device_client : DeviceClient,       # pylint: disable=redefined-outer-name
    device_service : DeviceService):    # pylint: disable=redefined-outer-name

    if not ENABLE_EMULATED: pytest.skip('Skipping test: No Emulated device has been configured')

    driver : _Driver = device_service.driver_instance_cache.get(DEVICE_EMU_UUID) # we know the driver exists now
    assert driver is not None

    driver_config = driver.GetConfig()
    #LOGGER.info('driver_config = {:s}'.format(str(driver_config)))

    DEVICE_EMU_WITH_DECONFIG_RULES = copy.deepcopy(DEVICE_EMU)
    DEVICE_EMU_WITH_DECONFIG_RULES['device_operational_status'] = \
        DeviceOperationalStatusEnum.DEVICEOPERATIONALSTATUS_DISABLED
    DEVICE_EMU_WITH_DECONFIG_RULES['device_config']['config_rules'].extend(DEVICE_EMU_DECONFIG_ADDRESSES)
    device_client.ConfigureDevice(Device(**DEVICE_EMU_WITH_DECONFIG_RULES))

    RESULTING_CONFIG_RULES = {cr['resource_key']:cr for cr in copy.deepcopy(DEVICE_EMU_CONFIG_ENDPOINTS)}
    for endpoint_cooked in DEVICE_EMU_ENDPOINTS_COOKED:
        values = json.loads(RESULTING_CONFIG_RULES[endpoint_cooked[0]]['resource_value'])
        values.update(endpoint_cooked[1])
        RESULTING_CONFIG_RULES[endpoint_cooked[0]]['resource_value'] = json.dumps(values, sort_keys=True)
    RESULTING_CONFIG_RULES = RESULTING_CONFIG_RULES.values()
    driver_config = sorted(driver.GetConfig(), key=operator.itemgetter(0))
    driver_config = json.loads(json.dumps(driver_config)) # prevent integer keys to fail matching with string keys
    driver_config = list(filter(
        lambda config_rule: (
            not isinstance(config_rule[1], str) or not config_rule[1].startswith('do_sampling (trigger:')),
        driver_config))
    LOGGER.info('driver_config = {:s}'.format(str(driver_config)))
    LOGGER.info('RESULTING_CONFIG_RULES = {:s}'.format(str(RESULTING_CONFIG_RULES)))
    assert len(driver_config) == len(RESULTING_CONFIG_RULES)
    for config_rule in RESULTING_CONFIG_RULES:
        config_rule = [config_rule['resource_key'], json.loads(config_rule['resource_value'])]
        #LOGGER.info('config_rule = {:s}'.format(str(config_rule)))
        assert config_rule in driver_config

    DEVICE_EMU_WITH_DECONFIG_RULES = copy.deepcopy(DEVICE_EMU)
    DEVICE_EMU_WITH_DECONFIG_RULES['device_config']['config_rules'].extend(DEVICE_EMU_DECONFIG_ENDPOINTS)
    device_client.ConfigureDevice(Device(**DEVICE_EMU_WITH_DECONFIG_RULES))

    driver_config = sorted(driver.GetConfig(), key=operator.itemgetter(0))
    driver_config = json.loads(json.dumps(driver_config)) # prevent integer keys to fail matching with string keys
    #LOGGER.info('driver_config = {:s}'.format(str(driver_config)))
    assert len(driver_config) == 0

    device_data = context_client.GetDevice(DeviceId(**DEVICE_EMU_ID))
    config_rules = device_data.device_config.config_rules
    LOGGER.info('config_rules = {:s}'.format(str(config_rules)))
    clean_config_rules = []
    for config_rule in config_rules:
        config_rule_value = json.loads(config_rule.resource_value)
        if not isinstance(config_rule_value, str): clean_config_rules.append(config_rule)
        if config_rule_value.startswith('do_sampling (trigger:'): continue
        clean_config_rules.append(config_rule)
    LOGGER.info('clean_config_rules = {:s}'.format(str(clean_config_rules)))
    assert len(clean_config_rules) == 0


def test_device_emulated_delete(
    context_client : ContextClient,     # pylint: disable=redefined-outer-name
    device_client : DeviceClient,       # pylint: disable=redefined-outer-name
    device_service : DeviceService):    # pylint: disable=redefined-outer-name

    if not ENABLE_EMULATED: pytest.skip('Skipping test: No Emulated device has been configured')

    device_client.DeleteDevice(DeviceId(**DEVICE_EMU_ID))
    driver : _Driver = device_service.driver_instance_cache.get(DEVICE_EMU_UUID, {})
    assert driver is None


# ----- Test Device Driver OpenConfig ------------------------------------------

def test_device_openconfig_add_error_cases(
    context_client : ContextClient,     # pylint: disable=redefined-outer-name
    device_client : DeviceClient,       # pylint: disable=redefined-outer-name
    device_service : DeviceService):    # pylint: disable=redefined-outer-name

    if not ENABLE_OPENCONFIG: pytest.skip('Skipping test: No OpenConfig device has been configured')

    with pytest.raises(grpc.RpcError) as e:
        DEVICE_OC_WITH_EXTRA_RULES = copy.deepcopy(DEVICE_OC)
        DEVICE_OC_WITH_EXTRA_RULES['device_config']['config_rules'].extend(DEVICE_OC_CONNECT_RULES)
        DEVICE_OC_WITH_EXTRA_RULES['device_config']['config_rules'].extend(DEVICE_OC_CONFIG_RULES)
        device_client.AddDevice(Device(**DEVICE_OC_WITH_EXTRA_RULES))
    assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
    msg_head = 'device.device_config.config_rules(['
    msg_tail = ']) is invalid; RPC method AddDevice only accepts connection Config Rules that should start '\
               'with "_connect/" tag. Others should be configured after adding the device.'
    except_msg = str(e.value.details())
    assert except_msg.startswith(msg_head) and except_msg.endswith(msg_tail)


def test_device_openconfig_add_correct(
    context_client : ContextClient,     # pylint: disable=redefined-outer-name
    device_client : DeviceClient,       # pylint: disable=redefined-outer-name
    device_service : DeviceService):    # pylint: disable=redefined-outer-name

    if not ENABLE_OPENCONFIG: pytest.skip('Skipping test: No OpenConfig device has been configured')

    DEVICE_OC_WITH_CONNECT_RULES = copy.deepcopy(DEVICE_OC)
    DEVICE_OC_WITH_CONNECT_RULES['device_config']['config_rules'].extend(DEVICE_OC_CONNECT_RULES)
    device_client.AddDevice(Device(**DEVICE_OC_WITH_CONNECT_RULES))
    driver : _Driver = device_service.driver_instance_cache.get(DEVICE_OC_UUID) # we know the driver exists now
    assert driver is not None

    device_data = context_client.GetDevice(DeviceId(**DEVICE_OC_ID))
    config_rules = [
        (ConfigActionEnum.Name(config_rule.action), config_rule.resource_key, config_rule.resource_value)
        for config_rule in device_data.device_config.config_rules
    ]
    LOGGER.info('device_data.device_config.config_rules = \n{:s}'.format(
        '\n'.join(['{:s} {:s} = {:s}'.format(*config_rule) for config_rule in config_rules])))


def test_device_openconfig_get(
    context_client : ContextClient,     # pylint: disable=redefined-outer-name
    device_client : DeviceClient,       # pylint: disable=redefined-outer-name
    device_service : DeviceService):    # pylint: disable=redefined-outer-name

    if not ENABLE_OPENCONFIG: pytest.skip('Skipping test: No OpenConfig device has been configured')

    initial_config = device_client.GetInitialConfig(DeviceId(**DEVICE_OC_ID))
    LOGGER.info('initial_config = {:s}'.format(grpc_message_to_json_string(initial_config)))

    device_data = context_client.GetDevice(DeviceId(**DEVICE_OC_ID))
    LOGGER.info('device_data = {:s}'.format(grpc_message_to_json_string(device_data)))


def test_device_openconfig_configure(
    context_client : ContextClient,     # pylint: disable=redefined-outer-name
    device_client : DeviceClient,       # pylint: disable=redefined-outer-name
    device_service : DeviceService):    # pylint: disable=redefined-outer-name

    if not ENABLE_OPENCONFIG: pytest.skip('Skipping test: No OpenConfig device has been configured')
    if not ENABLE_OPENCONFIG_CONFIGURE: pytest.skip('Skipping test OpenConfig configure')

    driver : _Driver = device_service.driver_instance_cache.get(DEVICE_OC_UUID) # we know the driver exists now
    assert driver is not None

    # Requires to retrieve data from device; might be slow. Uncomment only when needed and test does not pass directly.
    #driver_config = sorted(driver.GetConfig(), key=operator.itemgetter(0))
    #LOGGER.info('driver_config = {:s}'.format(str(driver_config)))

    DEVICE_OC_WITH_CONFIG_RULES = copy.deepcopy(DEVICE_OC)
    DEVICE_OC_WITH_CONFIG_RULES['device_config']['config_rules'].extend(DEVICE_OC_CONFIG_RULES)
    device_client.ConfigureDevice(Device(**DEVICE_OC_WITH_CONFIG_RULES))

    # Requires to retrieve data from device; might be slow. Uncomment only when needed and test does not pass directly.
    #driver_config = sorted(driver.GetConfig(), key=operator.itemgetter(0))
    #LOGGER.info('driver_config = {:s}'.format(str(driver_config)))

    device_data = context_client.GetDevice(DeviceId(**DEVICE_OC_ID))
    config_rules = [
        (ConfigActionEnum.Name(config_rule.action), config_rule.resource_key, config_rule.resource_value)
        for config_rule in device_data.device_config.config_rules
    ]
    LOGGER.info('device_data.device_config.config_rules = \n{:s}'.format(
        '\n'.join(['{:s} {:s} = {:s}'.format(*config_rule) for config_rule in config_rules])))
    for config_rule in DEVICE_OC_CONFIG_RULES:
        config_rule = (
            ConfigActionEnum.Name(config_rule['action']), config_rule['resource_key'], config_rule['resource_value'])
        assert config_rule in config_rules


def test_device_openconfig_monitor(
    context_client : ContextClient,                 # pylint: disable=redefined-outer-name
    device_client : DeviceClient,                   # pylint: disable=redefined-outer-name
    device_service : DeviceService,                 # pylint: disable=redefined-outer-name
    monitoring_service : MockMonitoringService):    # pylint: disable=redefined-outer-name

    if not ENABLE_OPENCONFIG: pytest.skip('Skipping test: No OpenConfig device has been configured')
    if not ENABLE_OPENCONFIG_MONITOR: pytest.skip('Skipping test OpenConfig monitor')

    device_uuid = DEVICE_OC_UUID
    json_device_id = DEVICE_OC_ID
    device_id = DeviceId(**json_device_id)
    device_data = context_client.GetDevice(device_id)
    #LOGGER.info('device_data = \n{:s}'.format(str(device_data)))

    driver : _Driver = device_service.driver_instance_cache.get(device_uuid) # we know the driver exists now
    assert driver is not None

    SAMPLING_DURATION_SEC = 60.0
    SAMPLING_INTERVAL_SEC = 15.0

    MONITORING_SETTINGS_LIST = []
    KPI_UUIDS__TO__NUM_SAMPLES_RECEIVED = {}
    for endpoint in device_data.device_endpoints:
        endpoint_uuid = endpoint.endpoint_id.endpoint_uuid.uuid
        for sample_type_id in endpoint.kpi_sample_types:
            sample_type_name = str(KpiSampleType.Name(sample_type_id)).upper().replace('KPISAMPLETYPE_', '')
            kpi_uuid = '{:s}-{:s}-{:s}-kpi_uuid'.format(device_uuid, endpoint_uuid, str(sample_type_id))
            monitoring_settings = {
                'kpi_id'        : {'kpi_id': {'uuid': kpi_uuid}},
                'kpi_descriptor': {
                    'kpi_description': 'Metric {:s} for Endpoint {:s} in Device {:s}'.format(
                        sample_type_name, endpoint_uuid, device_uuid),
                    'kpi_sample_type': sample_type_id,
                    'device_id': json_device_id,
                    'endpoint_id': json_endpoint_id(json_device_id, endpoint_uuid),
                },
                'sampling_duration_s': SAMPLING_DURATION_SEC,
                'sampling_interval_s': SAMPLING_INTERVAL_SEC,
            }
            MONITORING_SETTINGS_LIST.append(monitoring_settings)
            KPI_UUIDS__TO__NUM_SAMPLES_RECEIVED[kpi_uuid] = 0

    NUM_SAMPLES_EXPECTED_PER_KPI = SAMPLING_DURATION_SEC / SAMPLING_INTERVAL_SEC
    NUM_SAMPLES_EXPECTED = len(MONITORING_SETTINGS_LIST) * NUM_SAMPLES_EXPECTED_PER_KPI

    # Start monitoring the device
    t_start_monitoring = datetime.timestamp(datetime.utcnow())
    for monitoring_settings in MONITORING_SETTINGS_LIST:
        device_client.MonitorDeviceKpi(MonitoringSettings(**monitoring_settings))

    # wait to receive the expected number of samples
    # if takes more than 1.5 times the sampling duration, assume there is an error
    time_ini = time.time()
    queue_samples : queue.Queue = monitoring_service.queue_samples
    received_samples = []
    while (len(received_samples) < NUM_SAMPLES_EXPECTED) and (time.time() - time_ini < SAMPLING_DURATION_SEC * 1.5):
        try:
            received_sample = queue_samples.get(block=True, timeout=SAMPLING_INTERVAL_SEC / NUM_SAMPLES_EXPECTED)
            #LOGGER.info('received_sample = {:s}'.format(str(received_sample)))
            received_samples.append(received_sample)
        except queue.Empty:
            continue

    t_end_monitoring = datetime.timestamp(datetime.utcnow())

    #LOGGER.info('received_samples = {:s}'.format(str(received_samples)))
    LOGGER.info('len(received_samples) = {:s}'.format(str(len(received_samples))))
    LOGGER.info('NUM_SAMPLES_EXPECTED = {:s}'.format(str(NUM_SAMPLES_EXPECTED)))
    #assert len(received_samples) == NUM_SAMPLES_EXPECTED
    for received_sample in received_samples:
        kpi_uuid = received_sample.kpi_id.kpi_id.uuid
        assert kpi_uuid in KPI_UUIDS__TO__NUM_SAMPLES_RECEIVED
        assert isinstance(received_sample.timestamp, str)
        try:
            timestamp = float(received_sample.timestamp)
        except ValueError:
            dt_time = dateutil.parser.isoparse(received_sample.timestamp).replace(tzinfo=timezone.utc)
            timestamp = float(calendar.timegm(dt_time.timetuple())) + (dt_time.microsecond / 1.e6)
        assert timestamp > t_start_monitoring
        assert timestamp < t_end_monitoring
        assert received_sample.kpi_value.HasField('floatVal') or received_sample.kpi_value.HasField('intVal')
        kpi_value = getattr(received_sample.kpi_value, received_sample.kpi_value.WhichOneof('value'))
        assert isinstance(kpi_value, (float, int))
        KPI_UUIDS__TO__NUM_SAMPLES_RECEIVED[kpi_uuid] += 1

    LOGGER.info('KPI_UUIDS__TO__NUM_SAMPLES_RECEIVED = {:s}'.format(str(KPI_UUIDS__TO__NUM_SAMPLES_RECEIVED)))
    # TODO: review why num_samples_received per KPI != NUM_SAMPLES_EXPECTED_PER_KPI
    #for kpi_uuid, num_samples_received in KPI_UUIDS__TO__NUM_SAMPLES_RECEIVED.items():
    #    assert num_samples_received == NUM_SAMPLES_EXPECTED_PER_KPI

    # Unsubscribe monitoring
    for kpi_uuid in KPI_UUIDS__TO__NUM_SAMPLES_RECEIVED.keys():
        MONITORING_SETTINGS_UNSUBSCRIBE = {
            'kpi_id'             : {'kpi_id': {'uuid': kpi_uuid}},
            'sampling_duration_s': -1, # negative value in sampling_duration_s or sampling_interval_s means unsibscribe
            'sampling_interval_s': -1, # kpi_id is mandatory to unsibscribe
        }
        device_client.MonitorDeviceKpi(MonitoringSettings(**MONITORING_SETTINGS_UNSUBSCRIBE))


def test_device_openconfig_deconfigure(
    context_client : ContextClient,     # pylint: disable=redefined-outer-name
    device_client : DeviceClient,       # pylint: disable=redefined-outer-name
    device_service : DeviceService):    # pylint: disable=redefined-outer-name

    if not ENABLE_OPENCONFIG: pytest.skip('Skipping test: No OpenConfig device has been configured')
    if not ENABLE_OPENCONFIG_DECONFIGURE: pytest.skip('Skipping test OpenConfig deconfigure')

    driver : _Driver = device_service.driver_instance_cache.get(DEVICE_OC_UUID) # we know the driver exists now
    assert driver is not None

    # Requires to retrieve data from device; might be slow. Uncomment only when needed and test does not pass directly.
    #driver_config = sorted(driver.GetConfig(), key=operator.itemgetter(0))
    #LOGGER.info('driver_config = {:s}'.format(str(driver_config)))

    DEVICE_OC_WITH_DECONFIG_RULES = copy.deepcopy(DEVICE_OC)
    DEVICE_OC_WITH_DECONFIG_RULES['device_config']['config_rules'].extend(DEVICE_OC_DECONFIG_RULES)
    device_client.ConfigureDevice(Device(**DEVICE_OC_WITH_DECONFIG_RULES))

    # Requires to retrieve data from device; might be slow. Uncomment only when needed and test does not pass directly.
    #driver_config = sorted(driver.GetConfig(), key=operator.itemgetter(0))
    #LOGGER.info('driver_config = {:s}'.format(str(driver_config)))

    device_data = context_client.GetDevice(DeviceId(**DEVICE_OC_ID))
    config_rules = [
        (ConfigActionEnum.Name(config_rule.action), config_rule.resource_key, config_rule.resource_value)
        for config_rule in device_data.device_config.config_rules
    ]
    LOGGER.info('device_data.device_config.config_rules = \n{:s}'.format(
        '\n'.join(['{:s} {:s} = {:s}'.format(*config_rule) for config_rule in config_rules])))
    for config_rule in DEVICE_OC_DECONFIG_RULES:
        action_set = ConfigActionEnum.Name(ConfigActionEnum.CONFIGACTION_SET)
        config_rule = (action_set, config_rule['resource_key'], config_rule['resource_value'])
        assert config_rule not in config_rules


def test_device_openconfig_delete(
    context_client : ContextClient,     # pylint: disable=redefined-outer-name
    device_client : DeviceClient,       # pylint: disable=redefined-outer-name
    device_service : DeviceService):    # pylint: disable=redefined-outer-name

    if not ENABLE_OPENCONFIG: pytest.skip('Skipping test: No OpenConfig device has been configured')

    device_client.DeleteDevice(DeviceId(**DEVICE_OC_ID))
    driver : _Driver = device_service.driver_instance_cache.get(DEVICE_OC_UUID, {})
    assert driver is None


# ----- Test Device Driver TAPI ------------------------------------------------

def test_device_tapi_add_error_cases(
    device_client : DeviceClient):      # pylint: disable=redefined-outer-name

    if not ENABLE_TAPI: pytest.skip('Skipping test: No TAPI device has been configured')

    with pytest.raises(grpc.RpcError) as e:
        DEVICE_TAPI_WITH_EXTRA_RULES = copy.deepcopy(DEVICE_TAPI)
        DEVICE_TAPI_WITH_EXTRA_RULES['device_config']['config_rules'].extend(DEVICE_TAPI_CONNECT_RULES)
        DEVICE_TAPI_WITH_EXTRA_RULES['device_config']['config_rules'].extend(DEVICE_TAPI_CONFIG_RULES)
        device_client.AddDevice(Device(**DEVICE_TAPI_WITH_EXTRA_RULES))
    assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
    msg_head = 'device.device_config.config_rules(['
    msg_tail = ']) is invalid; RPC method AddDevice only accepts connection Config Rules that should start '\
               'with "_connect/" tag. Others should be configured after adding the device.'
    except_msg = str(e.value.details())
    assert except_msg.startswith(msg_head) and except_msg.endswith(msg_tail)


def test_device_tapi_add_correct(
    device_client: DeviceClient,        # pylint: disable=redefined-outer-name
    device_service: DeviceService):     # pylint: disable=redefined-outer-name

    if not ENABLE_TAPI: pytest.skip('Skipping test: No TAPI device has been configured')

    DEVICE_TAPI_WITH_CONNECT_RULES = copy.deepcopy(DEVICE_TAPI)
    DEVICE_TAPI_WITH_CONNECT_RULES['device_config']['config_rules'].extend(DEVICE_TAPI_CONNECT_RULES)
    device_client.AddDevice(Device(**DEVICE_TAPI_WITH_CONNECT_RULES))
    driver: _Driver = device_service.driver_instance_cache.get(DEVICE_TAPI_UUID)
    assert driver is not None


def test_device_tapi_get(
    context_client: ContextClient,      # pylint: disable=redefined-outer-name
    device_client: DeviceClient):       # pylint: disable=redefined-outer-name

    if not ENABLE_TAPI: pytest.skip('Skipping test: No TAPI device has been configured')

    initial_config = device_client.GetInitialConfig(DeviceId(**DEVICE_TAPI_ID))
    LOGGER.info('initial_config = {:s}'.format(grpc_message_to_json_string(initial_config)))

    device_data = context_client.GetDevice(DeviceId(**DEVICE_TAPI_ID))
    LOGGER.info('device_data = {:s}'.format(grpc_message_to_json_string(device_data)))


def test_device_tapi_configure(
    context_client: ContextClient,      # pylint: disable=redefined-outer-name
    device_client: DeviceClient,        # pylint: disable=redefined-outer-name
    device_service: DeviceService):     # pylint: disable=redefined-outer-name

    if not ENABLE_TAPI: pytest.skip('Skipping test: No TAPI device has been configured')

    driver : _Driver = device_service.driver_instance_cache.get(DEVICE_TAPI_UUID)
    assert driver is not None

    # Requires to retrieve data from device; might be slow. Uncomment only when needed and test does not pass directly.
    #driver_config = sorted(driver.GetConfig(), key=operator.itemgetter(0))
    #LOGGER.info('driver_config = {:s}'.format(str(driver_config)))

    DEVICE_TAPI_WITH_CONFIG_RULES = copy.deepcopy(DEVICE_TAPI)
    DEVICE_TAPI_WITH_CONFIG_RULES['device_config']['config_rules'].extend(DEVICE_TAPI_CONFIG_RULES)
    device_client.ConfigureDevice(Device(**DEVICE_TAPI_WITH_CONFIG_RULES))

    # Requires to retrieve data from device; might be slow. Uncomment only when needed and test does not pass directly.
    #driver_config = sorted(driver.GetConfig(), key=operator.itemgetter(0))
    #LOGGER.info('driver_config = {:s}'.format(str(driver_config)))

    device_data = context_client.GetDevice(DeviceId(**DEVICE_TAPI_ID))
    config_rules = [
        (ConfigActionEnum.Name(config_rule.action), config_rule.resource_key, config_rule.resource_value)
        for config_rule in device_data.device_config.config_rules
    ]
    LOGGER.info('device_data.device_config.config_rules = \n{:s}'.format(
        '\n'.join(['{:s} {:s} = {:s}'.format(*config_rule) for config_rule in config_rules])))
    for config_rule in DEVICE_TAPI_CONFIG_RULES:
        config_rule = (
            ConfigActionEnum.Name(config_rule['action']), config_rule['resource_key'], config_rule['resource_value'])
        assert config_rule in config_rules


def test_device_tapi_deconfigure(
    context_client: ContextClient,      # pylint: disable=redefined-outer-name
    device_client: DeviceClient,        # pylint: disable=redefined-outer-name
    device_service: DeviceService):     # pylint: disable=redefined-outer-name

    if not ENABLE_TAPI: pytest.skip('Skipping test: No TAPI device has been configured')

    driver: _Driver = device_service.driver_instance_cache.get(DEVICE_TAPI_UUID)
    assert driver is not None

    # Requires to retrieve data from device; might be slow. Uncomment only when needed and test does not pass directly.
    #driver_config = sorted(driver.GetConfig(), key=operator.itemgetter(0))
    #LOGGER.info('driver_config = {:s}'.format(str(driver_config)))

    DEVICE_TAPI_WITH_DECONFIG_RULES = copy.deepcopy(DEVICE_TAPI)
    DEVICE_TAPI_WITH_DECONFIG_RULES['device_config']['config_rules'].extend(DEVICE_TAPI_DECONFIG_RULES)
    device_client.ConfigureDevice(Device(**DEVICE_TAPI_WITH_DECONFIG_RULES))

    # Requires to retrieve data from device; might be slow. Uncomment only when needed and test does not pass directly.
    #driver_config = sorted(driver.GetConfig(), key=operator.itemgetter(0))
    #LOGGER.info('driver_config = {:s}'.format(str(driver_config)))

    device_data = context_client.GetDevice(DeviceId(**DEVICE_TAPI_ID))
    config_rules = [
        (ConfigActionEnum.Name(config_rule.action), config_rule.resource_key, config_rule.resource_value)
        for config_rule in device_data.device_config.config_rules
    ]
    LOGGER.info('device_data.device_config.config_rules = \n{:s}'.format(
        '\n'.join(['{:s} {:s} = {:s}'.format(*config_rule) for config_rule in config_rules])))
    for config_rule in DEVICE_TAPI_DECONFIG_RULES:
        action_set = ConfigActionEnum.Name(ConfigActionEnum.CONFIGACTION_SET)
        config_rule = (action_set, config_rule['resource_key'], config_rule['resource_value'])
        assert config_rule not in config_rules


def test_device_tapi_delete(
    device_client : DeviceClient,       # pylint: disable=redefined-outer-name
    device_service : DeviceService):    # pylint: disable=redefined-outer-name

    if not ENABLE_TAPI: pytest.skip('Skipping test: No TAPI device has been configured')

    device_client.DeleteDevice(DeviceId(**DEVICE_TAPI_ID))
    driver : _Driver = device_service.driver_instance_cache.get(DEVICE_TAPI_UUID, {})
    assert driver is None


# ----- Test Device Driver P4 --------------------------------------------------

def test_device_p4_add_error_cases(
        context_client: ContextClient,   # pylint: disable=redefined-outer-name
        device_client: DeviceClient,     # pylint: disable=redefined-outer-name
        device_service: DeviceService):  # pylint: disable=redefined-outer-name

    if not ENABLE_P4: pytest.skip('Skipping test: No P4 device has been configured')

    with pytest.raises(grpc.RpcError) as e:
        device_p4_with_extra_rules = copy.deepcopy(DEVICE_P4)
        device_p4_with_extra_rules['device_config']['config_rules'].extend(
            DEVICE_P4_CONNECT_RULES)
        device_p4_with_extra_rules['device_config']['config_rules'].extend(
            DEVICE_P4_CONFIG_RULES)
        device_client.AddDevice(Device(**device_p4_with_extra_rules))
    assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
    msg_head = 'device.device_config.config_rules(['
    msg_tail = ']) is invalid; RPC method AddDevice only accepts connection Config Rules that should start '\
               'with "_connect/" tag. Others should be configured after adding the device.'
    except_msg = str(e.value.details())
    assert except_msg.startswith(msg_head) and except_msg.endswith(msg_tail)


def test_device_p4_add_correct(
        context_client: ContextClient,              # pylint: disable=redefined-outer-name
        device_client: DeviceClient,                # pylint: disable=redefined-outer-name
        device_service: DeviceService,              # pylint: disable=redefined-outer-name
        p4runtime_service: MockP4RuntimeService):   # pylint: disable=redefined-outer-name

    if not ENABLE_P4: pytest.skip('Skipping test: No P4 device has been configured')

    device_p4_with_connect_rules = copy.deepcopy(DEVICE_P4)
    device_p4_with_connect_rules['device_config']['config_rules'].extend(
        DEVICE_P4_CONNECT_RULES)
    device_client.AddDevice(Device(**device_p4_with_connect_rules))
    driver : _Driver = device_service.driver_instance_cache.get(DEVICE_P4_NAME)
    assert driver is not None


def test_device_p4_get(
        context_client: ContextClient,              # pylint: disable=redefined-outer-name
        device_client: DeviceClient,                # pylint: disable=redefined-outer-name
        device_service: DeviceService,              # pylint: disable=redefined-outer-name
        p4runtime_service: MockP4RuntimeService):   # pylint: disable=redefined-outer-name

    if not ENABLE_P4: pytest.skip('Skipping test: No P4 device has been configured')

    initial_config = device_client.GetInitialConfig(DeviceId(**DEVICE_P4_UUID))
    LOGGER.info('initial_config = {:s}'.format(
        grpc_message_to_json_string(initial_config)))

    device_data = context_client.GetDevice(DeviceId(**DEVICE_P4_UUID))
    LOGGER.info('device_data = {:s}'.format(
        grpc_message_to_json_string(device_data)))


def test_device_p4_configure(
        context_client: ContextClient,              # pylint: disable=redefined-outer-name
        device_client: DeviceClient,                # pylint: disable=redefined-outer-name
        device_service: DeviceService,              # pylint: disable=redefined-outer-name
        p4runtime_service: MockP4RuntimeService):   # pylint: disable=redefined-outer-name
    pytest.skip('Skipping test for unimplemented method')


def test_device_p4_deconfigure(
        context_client: ContextClient,              # pylint: disable=redefined-outer-name
        device_client: DeviceClient,                # pylint: disable=redefined-outer-name
        device_service: DeviceService,              # pylint: disable=redefined-outer-name
        p4runtime_service: MockP4RuntimeService):   # pylint: disable=redefined-outer-name
    pytest.skip('Skipping test for unimplemented method')


def test_device_p4_delete(
        context_client: ContextClient,              # pylint: disable=redefined-outer-name
        device_client: DeviceClient,                # pylint: disable=redefined-outer-name
        device_service: DeviceService,              # pylint: disable=redefined-outer-name
        p4runtime_service: MockP4RuntimeService):   # pylint: disable=redefined-outer-name

    if not ENABLE_P4: pytest.skip('Skipping test: No P4 device has been configured')

    device_client.DeleteDevice(DeviceId(**DEVICE_P4_UUID))
    driver : _Driver = device_service.driver_instance_cache.get(DEVICE_P4_NAME)
    assert driver is None
