import copy, grpc, json, logging, operator, os, pytest, time
from datetime import datetime
from typing import Tuple
from queue import Queue, Empty
from google.protobuf.json_format import MessageToDict
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 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 device.tests.Tools import endpoint, endpoint_id
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_ENDPOINTS, DEVICE_EMU_ENDPOINTS_COOKED,
    DEVICE_EMU_ID, DEVICE_EMU_RECONFIG_ADDRESSES, DEVICE_EMU_UUID)
try:
    from .Device_OpenConfig_Infinera 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
    # Create a Device_OpenConfig_??.py file with the details for your device to test it and import it as follows in the
    # try block of this import statement.
    #   from .Device_OpenConfig_?? import(
    #       DEVICE_OC, DEVICE_OC_CONFIG_RULES, DEVICE_OC_DECONFIG_RULES, DEVICE_OC_CONNECT_RULES, DEVICE_OC_ID,
    #       DEVICE_OC_UUID)

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
    # Create a Device_Transport_Api_??.py file with the details for your device to test it and import it as follows in
    # the try block of this import statement.
    #   from .Device_Transport_Api_?? import(
    #       DEVICE_TAPI, DEVICE_TAPI_CONFIG_RULES, DEVICE_TAPI_DECONFIG_RULES, DEVICE_TAPI_CONNECT_RULES,
    #       DEVICE_TAPI_ID, DEVICE_TAPI_UUID)

#ENABLE_OPENCONFIG = False
#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)
except ImportError:
    raise ImportError("Test configuration for P4 devices not found")

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 grpc_message_to_json_string(message):
    return str(MessageToDict(
        message, including_default_value_fields=True, preserving_proto_field_name=True, use_integers_for_enums=False))


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

    with pytest.raises(grpc.RpcError) as e:
        DEVICE_EMU_WITH_ENDPOINTS = copy.deepcopy(DEVICE_EMU)
        DEVICE_EMU_WITH_ENDPOINTS['device_endpoints'].append(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

    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

    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

    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

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

    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) + len(DEVICE_EMU_CONFIG_ADDRESSES)

    SAMPLING_DURATION_SEC = 3.0
    SAMPLING_INTERVAL_SEC = 0.5

    MONITORING_SETTINGS_LIST = []
    KPI_UUIDS__TO__NUM_SAMPLES_RECEIVED = {}
    for endpoint_uuid,_,sample_types in DEVICE_EMU_ENDPOINTS:
        for sample_type in sample_types:
            sample_type_id = sample_type.value
            sample_type_name = str(KpiSampleType.Name(sample_type_id)).upper().replace('KPISAMPLETYPE_', '')
            kpi_uuid = '{:s}-{:s}-{:s}-kpi_uuid'.format(DEVICE_EMU_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_EMU_UUID),
                    'kpi_sample_type': sample_type_id,
                    'device_id': DEVICE_EMU_ID,
                    'endpoint_id': endpoint_id(DEVICE_EMU_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 = 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 Empty:
            continue

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

    LOGGER.info('received_samples = {:s}'.format(str(received_samples)))
    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)
        timestamp = float(received_sample.timestamp)
        assert timestamp > t_start_monitoring
        assert timestamp < t_end_monitoring
        assert received_sample.kpi_value.HasField('floatVal')
        assert isinstance(received_sample.kpi_value.floatVal, float)
        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

    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
    #LOGGER.info('driver_config = {:s}'.format(str(driver_config)))
    assert len(driver_config) == len(RESULTING_CONFIG_RULES)
    #LOGGER.info('RESULTING_CONFIG_RULES = {:s}'.format(str(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))
    assert len(device_data.device_config.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

    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: return # if there is no device to test against, asusme test is silently passed.

    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: return # if there is no device to test against, asusme test is silently passed.

    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


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: return # if there is no device to test against, asusme test is silently passed.

    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: return # if there is no device to test against, asusme test is silently passed.

    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_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: return # if there is no device to test against, asusme test is silently passed.

    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: return # if there is no device to test against, asusme test is silently passed.

    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: return # if there is no device to test against, asusme test is silently passed.

    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: return # if there is no device to test against, asusme test is silently passed.

    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: return # if there is no device to test against, asusme test is silently passed.

    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: return # if there is no device to test against, asusme test is silently passed.

    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: return # if there is no device to test against, asusme test is silently passed.

    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: return # if there is no device to test against, asusme test is silently passed.

    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

    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):

    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):

    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):
    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):
    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):

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