import copy, grpc, logging, pytest
from google.protobuf.json_format import MessageToDict
from common.database.Factory import get_database, DatabaseEngineEnum
from common.database.api.context.OperationalStatus import OperationalStatus
from common.tests.Assertions import validate_device_id, validate_empty
from device.client.DeviceClient import DeviceClient
from device.proto.context_pb2 import Device, DeviceId
from device.service.DeviceService import DeviceService
from device.Config import GRPC_SERVICE_PORT, GRPC_MAX_WORKERS, GRPC_GRACE_PERIOD

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

DEVICE_ID = {'device_id': {'uuid': 'test-device-001'}}
DEVICE = {
    'device_id': {'device_id': {'uuid': 'test-device-001'}},
    'device_type': 'ROADM',
    'device_config': {'device_config': ''},
    'devOperationalStatus': 1,
    'endpointList' : [
        {
            'port_id': {
                'topoId': {
                    'contextId': {'contextUuid': {'uuid': 'admin'}},
                    'topoId': {'uuid': 'admin'}
                },
                'dev_id': {'device_id': {'uuid': 'test-device-001'}},
                'port_id': {'uuid' : 'port-101'}
            },
            'port_type': 'LINE'
        },
        {
            'port_id': {
                'topoId': {
                    'contextId': {'contextUuid': {'uuid': 'admin'}},
                    'topoId': {'uuid': 'admin'}
                },
                'dev_id': {'device_id': {'uuid': 'test-device-001'}},
                'port_id': {'uuid' : 'port-102'}
            },
            'port_type': 'LINE'
        },
    ]
}

@pytest.fixture(scope='session')
def service():
    database = get_database(engine=DatabaseEngineEnum.INMEMORY)
    _service = DeviceService(
        database, port=GRPC_SERVICE_PORT, max_workers=GRPC_MAX_WORKERS, grace_period=GRPC_GRACE_PERIOD)
    _service.start()
    yield _service
    _service.stop()

@pytest.fixture(scope='session')
def client(service):
    _client = DeviceClient(address='127.0.0.1', port=GRPC_SERVICE_PORT)
    yield _client
    _client.close()

def test_create_empty_device_uuid(client : DeviceClient):
    # should fail with device uuid is empty
    with pytest.raises(grpc._channel._InactiveRpcError) as e:
        copy_device = copy.deepcopy(DEVICE)
        copy_device['device_id']['device_id']['uuid'] = ''
        client.AddDevice(Device(**copy_device))
    assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
    assert e.value.details() == 'device_uuid() string is empty.'

def test_create_empty_device_type(client : DeviceClient):
    # should fail with device type is empty
    with pytest.raises(grpc._channel._InactiveRpcError) as e:
        copy_device = copy.deepcopy(DEVICE)
        copy_device['device_type'] = ''
        client.AddDevice(Device(**copy_device))
    assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
    assert e.value.details() == 'device_type() string is empty.'

def test_create_wrong_device_operational_status(client : DeviceClient):
    # should fail with wrong device operational status
    with pytest.raises(grpc._channel._InactiveRpcError) as e:
        copy_device = copy.deepcopy(DEVICE)
        copy_device['devOperationalStatus'] = OperationalStatus.KEEP_STATE.value
        client.AddDevice(Device(**copy_device))
    assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
    msg = ' '.join([
        'Device has to be created with either ENABLED/DISABLED Operational State.',
        'Use KEEP_STATE only in configure Device methods.',
    ])
    assert e.value.details() == msg

def test_create_endpoint_wrong_context(client : DeviceClient):
    # should fail with unsupported context
    with pytest.raises(grpc._channel._InactiveRpcError) as e:
        copy_device = copy.deepcopy(DEVICE)
        copy_device['endpointList'][0]['port_id']['topoId']['contextId']['contextUuid']['uuid'] = 'wrong-context'
        request = Device(**copy_device)
        LOGGER.warning('request = {}'.format(request))
        client.AddDevice(request)
    assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
    msg = ' '.join([
        'Unsupported context_id(wrong-context) in endpoint #0.',
        'Only default context_id(admin) is currently supported.',
        'Optionally, leave field empty to use default context_id.',
    ])
    assert e.value.details() == msg

def test_create_endpoint_wrong_topology(client : DeviceClient):
    # should fail with unsupported topology
    with pytest.raises(grpc._channel._InactiveRpcError) as e:
        copy_device = copy.deepcopy(DEVICE)
        copy_device['endpointList'][0]['port_id']['topoId']['topoId']['uuid'] = 'wrong-topo'
        client.AddDevice(Device(**copy_device))
    assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
    msg = ' '.join([
        'Unsupported topology_id(wrong-topo) in endpoint #0.',
        'Only default topology_id(admin) is currently supported.',
        'Optionally, leave field empty to use default topology_id.',
    ])
    assert e.value.details() == msg

def test_create_endpoint_wrong_device(client : DeviceClient):
    # should fail with wrong endpoint device
    with pytest.raises(grpc._channel._InactiveRpcError) as e:
        copy_device = copy.deepcopy(DEVICE)
        copy_device['endpointList'][0]['port_id']['dev_id']['device_id']['uuid'] = 'wrong-device'
        client.AddDevice(Device(**copy_device))
    assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
    msg = ' '.join([
        'Wrong device_id(wrong-device) in endpoint #0.',
        'Parent specified in message is device_id(test-device-001).',
        'Optionally, leave field empty to use parent device_id.',
    ])
    assert e.value.details() == msg

def test_create_empty_port_uuid(client : DeviceClient):
    # should fail with port uuid is empty
    with pytest.raises(grpc._channel._InactiveRpcError) as e:
        copy_device = copy.deepcopy(DEVICE)
        copy_device['endpointList'][0]['port_id']['port_id']['uuid'] = ''
        client.AddDevice(Device(**copy_device))
    assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
    assert e.value.details() == 'port_uuid() string is empty.'

def test_create_empty_port_type(client : DeviceClient):
    # should fail with port type is empty
    with pytest.raises(grpc._channel._InactiveRpcError) as e:
        copy_device = copy.deepcopy(DEVICE)
        copy_device['endpointList'][0]['port_type'] = ''
        client.AddDevice(Device(**copy_device))
    assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
    assert e.value.details() == 'port_type() string is empty.'

def test_create_duplicate_port(client : DeviceClient):
    # should fail with uplicate port in device
    with pytest.raises(grpc._channel._InactiveRpcError) as e:
        copy_device = copy.deepcopy(DEVICE)
        copy_device['endpointList'][1]['port_id']['port_id']['uuid'] = 'port-101'
        client.AddDevice(Device(**copy_device))
    assert e.value.code() == grpc.StatusCode.ALREADY_EXISTS
    assert e.value.details() == 'Duplicated port_id(port-101) in device_id(test-device-001).'

def test_create(client : DeviceClient):
    # should work
    validate_device_id(MessageToDict(
            client.AddDevice(Device(**DEVICE)),
            including_default_value_fields=True, preserving_proto_field_name=True,
            use_integers_for_enums=False))

def test_create_duplicate(client : DeviceClient):
    # should fail with device already exists
    with pytest.raises(grpc._channel._InactiveRpcError) as e:
        client.AddDevice(Device(**DEVICE))
    assert e.value.code() == grpc.StatusCode.ALREADY_EXISTS
    assert e.value.details() == 'device_uuid(test-device-001) already exists.'

def test_delete_empty_uuid(client : DeviceClient):
    # should fail with device uuid is empty
    with pytest.raises(grpc._channel._InactiveRpcError) as e:
        copy_device_id = copy.deepcopy(DEVICE_ID)
        copy_device_id['device_id']['uuid'] = ''
        client.DeleteDevice(DeviceId(**copy_device_id))
    assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
    assert e.value.details() == 'device_uuid() string is empty.'

def test_delete_device_not_found(client : DeviceClient):
    # should fail with device not found
    with pytest.raises(grpc._channel._InactiveRpcError) as e:
        copy_device_id = copy.deepcopy(DEVICE_ID)
        copy_device_id['device_id']['uuid'] = 'wrong-device-id'
        client.DeleteDevice(DeviceId(**copy_device_id))
    assert e.value.code() == grpc.StatusCode.NOT_FOUND
    assert e.value.details() == 'device_uuid(wrong-device-id) does not exist.'

def test_delete(client : DeviceClient):
    # should work
    validate_empty(MessageToDict(
            client.DeleteDevice(DeviceId(**DEVICE_ID)),
            including_default_value_fields=True, preserving_proto_field_name=True,
            use_integers_for_enums=False))

def test_configure_empty_device_uuid(client : DeviceClient):
    # should fail with device uuid is empty
    with pytest.raises(grpc._channel._InactiveRpcError) as e:
        copy_device = copy.deepcopy(DEVICE)
        copy_device['device_id']['device_id']['uuid'] = ''
        client.ConfigureDevice(Device(**copy_device))
    assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
    assert e.value.details() == 'device_uuid() string is empty.'

def test_configure_device_not_found(client : DeviceClient):
    # should fail with device not found
    with pytest.raises(grpc._channel._InactiveRpcError) as e:
        copy_device = copy.deepcopy(DEVICE)
        copy_device['device_id']['device_id']['uuid'] = 'wrong-device-id'
        client.ConfigureDevice(Device(**copy_device))
    assert e.value.code() == grpc.StatusCode.NOT_FOUND
    assert e.value.details() == 'device_uuid(wrong-device-id) does not exist.'

def test_create_device_default_endpoint_context_topology(client : DeviceClient):
    # should work
    copy_device = copy.deepcopy(DEVICE)
    copy_device['endpointList'][0]['port_id']['topoId']['contextId']['contextUuid']['uuid'] = ''
    copy_device['endpointList'][0]['port_id']['topoId']['topoId']['uuid'] = ''
    copy_device['endpointList'][0]['port_id']['dev_id']['device_id']['uuid'] = ''
    validate_device_id(MessageToDict(
            client.AddDevice(Device(**copy_device)),
            including_default_value_fields=True, preserving_proto_field_name=True,
            use_integers_for_enums=False))

def test_configure_wrong_device_type(client : DeviceClient):
    # should fail with device type is wrong
    with pytest.raises(grpc._channel._InactiveRpcError) as e:
        copy_device = copy.deepcopy(DEVICE)
        copy_device['device_type'] = 'wrong-type'
        client.ConfigureDevice(Device(**copy_device))
    assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
    assert e.value.details() == 'Device(test-device-001) has Type(ROADM). Cannot be changed to Type(wrong-type).'

def test_configure_with_endpoints(client : DeviceClient):
    # should fail with endpoints cannot be modified
    with pytest.raises(grpc._channel._InactiveRpcError) as e:
        copy_device = copy.deepcopy(DEVICE)
        client.ConfigureDevice(Device(**copy_device))
    assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
    assert e.value.details() == 'Endpoints belonging to Device(test-device-001) cannot be modified.'

def test_configure_no_change(client : DeviceClient):
    # should fail with any change detected
    with pytest.raises(grpc._channel._InactiveRpcError) as e:
        copy_device = copy.deepcopy(DEVICE)
        copy_device['devOperationalStatus'] = OperationalStatus.KEEP_STATE.value
        copy_device['endpointList'].clear()
        client.ConfigureDevice(Device(**copy_device))
    assert e.value.code() == grpc.StatusCode.ABORTED
    msg = ' '.join([
        'Any change has been requested for Device(test-device-001).',
        'Either specify a new configuration or a new device state.',
    ])
    assert e.value.details() == msg

def test_configure(client : DeviceClient):
    # should work
    copy_device = copy.deepcopy(DEVICE)
    copy_device['device_config']['device_config'] = '<new_config/>'
    copy_device['devOperationalStatus'] = OperationalStatus.DISABLED.value
    copy_device['endpointList'].clear()
    validate_device_id(MessageToDict(
            client.ConfigureDevice(Device(**copy_device)),
            including_default_value_fields=True, preserving_proto_field_name=True,
            use_integers_for_enums=False))
