"""
# 
#   Copyright 2016 RIFT.IO Inc
#
#   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.
#

@file cal_test.py
@author Varun Prasad (varun.prasad@riftio.com)
@date 22-Jan-2016

"""

import abc
import logging
import os
import multiprocessing
import signal
import time
import uuid
import hashlib

import pytest

from gi import require_version
require_version('RwCal', '1.0')

from gi.repository import RwcalYang
from gi.repository.RwTypes import RwStatus
# import rift.cal.server as cal_server
import rw_peas
import rwlogger
        

logger = logging.getLogger('rwcal')
logging.basicConfig(level=logging.INFO)


class CloudConfig(object):
    def __init__(self, cal, account):
        self.cal = cal
        self.account = account

    def check_state(self, object_id, object_api, expected_state, state_attr_name="state"):
        """For a given object (Vm, port etc) checks if the object has
        reached the expected state.
        """
        get_object = getattr(self.cal, object_api)
        for i in range(100):  # 100 poll iterations...
            rc, rs = get_object(self.account, object_id)

            curr_state = getattr(rs, state_attr_name)
            if curr_state == expected_state:
                break
            else:
                time.sleep(2)

        rc, rs = get_object(self.account, object_id)
        assert rc == RwStatus.SUCCESS
        assert getattr(rs, state_attr_name) == expected_state

    def start_server(self):
        pass

    def stop_server(self):
        pass

    @abc.abstractmethod
    def _cal(self):
        pass

    @abc.abstractmethod
    def _account(self, option):
        pass

    @abc.abstractmethod
    def flavor(self):
        pass

    @abc.abstractmethod
    def vdu(self):
        pass

    @abc.abstractmethod
    def image(self):
        pass

    @abc.abstractmethod
    def virtual_link(self):
        pass


class Aws(CloudConfig):
    def __init__(self, option):
        """
        Args:
            option (OptionParser): OptionParser instance.
        """
        self.image_id = 'ami-7070231a'
        self.virtual_link_id = None
        self.flavor_id = None
        self.vdu_id = None

        super().__init__(self._cal(), self._account(option))

    def _cal(self):
        """
        Loads rw.cal plugin via libpeas
        """
        plugin = rw_peas.PeasPlugin('rwcal_aws', 'RwCal-1.0')

        engine, info, extension = plugin()

        # Get the RwLogger context
        rwloggerctx = rwlogger.RwLog.Ctx.new("Cal-Log")

        cal = plugin.get_interface("Cloud")
        try:
            rc = cal.init(rwloggerctx)
            assert rc == RwStatus.SUCCESS
        except:
            logger.error("ERROR:Cal plugin instantiation failed. Aborting tests")
        else:
            logger.info("AWS Cal plugin successfully instantiated")
        return cal

    def _account(self, option):
        """
        Args:
            option (OptionParser): OptionParser instance.

        Return:
            CloudAccount details
        """
        account = RwcalYang.CloudAccount.from_dict({
                "account_type": "aws",
                "aws": {
                    "key": option.aws_user,
                    "secret": option.aws_password,
                    "region": option.aws_region,
                    "availability_zone": option.aws_zone,
                    "ssh_key": option.aws_ssh_key
                }
            })

        return account

    def flavor(self):
        """
        Returns:
            FlavorInfoItem
        """
        flavor = RwcalYang.FlavorInfoItem.from_dict({
                    "name": str(uuid.uuid4()),
                    "vm_flavor": {
                        "memory_mb": 1024,
                        "vcpu_count": 1,
                        "storage_gb": 0
                    }
            })

        return flavor

    def vdu(self):
        """Provide AWS specific VDU config.

        Returns:
            VDUInitParams
        """
        vdu = RwcalYang.VDUInitParams.from_dict({
                "name": str(uuid.uuid4()),
                "node_id": "123456789012345",
                "image_id": self.image_id,
                "flavor_id": "t2.micro"
            })

        c1 = vdu.connection_points.add()
        c1.name = str(uuid.uuid4())
        c1.virtual_link_id = self.virtual_link_id

        return vdu

    def image(self):
        raise NotImplementedError("Image create APIs are not implemented for AWS")

    def virtual_link(self):
        """Provide Vlink config

        Returns:
            VirtualLinkReqParams
        """
        vlink = RwcalYang.VirtualLinkReqParams.from_dict({
                    "name": str(uuid.uuid4()),
                    "subnet": '172.31.64.0/20',
            })

        return vlink


class Cloudsim(CloudConfig):
    def __init__(self, option):
        self.image_id = None
        self.virtual_link_id = None
        self.flavor_id = None
        self.vdu_id = None

        self.server_process = None


        super().__init__(self._cal(), self._account(option))

    def _md5(fname, blksize=1048576):
        hash_md5 = hashlib.md5()
        with open(fname, "rb") as f:
            for chunk in iter(lambda: f.read(blksize), b""):
                hash_md5.update(chunk)
        return hash_md5.hexdigest()
                                    
    def start_server(self):
        logger = logging.getLogger(__name__)
        server = cal_server.CloudsimServerOperations(logger)
        self.server_process = multiprocessing.Process(
                target=server.start_server,
                args=(True,))
        self.server_process.start()

        # Sleep till the backup store is set up
        time.sleep(30)

    def stop_server(self):
        self.server_process.terminate()

        # If the process is not killed within the timeout, send a SIGKILL.
        time.sleep(15)
        if self.server_process.is_alive():
            os.kill(self.server_process.pid, signal.SIGKILL)

    def _cal(self):
        """
        Loads rw.cal plugin via libpeas
        """
        plugin = rw_peas.PeasPlugin('rwcal_cloudsimproxy', 'RwCal-1.0')
        engine, info, extension = plugin()

        # Get the RwLogger context
        rwloggerctx = rwlogger.RwLog.Ctx.new("Cal-Log")

        cal = plugin.get_interface("Cloud")
        try:
            rc = cal.init(rwloggerctx)
            assert rc == RwStatus.SUCCESS
        except:
            logger.error("ERROR:Cal plugin instantiation failed. Aborting tests")
        else:
            logger.info("Cloudsim Cal plugin successfully instantiated")
        return cal

    def _account(self, option):
        """
        Args:
            option (OptionParser): OptionParser instance.

        Return:
            CloudAccount details
        """
        account = RwcalYang.CloudAccount.from_dict({
                'name': "cloudsim",
                'account_type':'cloudsim_proxy'})

        return account

    def image(self):
        """Provides Image config for openstack.

        Returns:
            ImageInfoItem
        """
        image = RwcalYang.ImageInfoItem.from_dict({
                "name": str(uuid.uuid4()),
                "location": os.path.join(os.getenv("RIFT_ROOT"), "images/rift-root-latest.qcow2"),
                "disk_format": "qcow2",
                "container_format": "bare",
                "checksum": self._md5(os.path.join(os.getenv("RIFT_ROOT"), "images/rift-root-latest.qcow2")),
            })
        return image

    def flavor(self):
        """Flavor config for openstack

        Returns:
            FlavorInfoItem
        """
        flavor = RwcalYang.FlavorInfoItem.from_dict({
                "name": str(uuid.uuid4()),
                "vm_flavor": {
                        "memory_mb": 16392,
                        "vcpu_count": 4,
                        "storage_gb": 40
                }})

        return flavor

    def vdu(self):
        """Returns VDU config

        Returns:
            VDUInitParams
        """
        vdu = RwcalYang.VDUInitParams.from_dict({
                "name": str(uuid.uuid4()),
                "node_id": "123456789012345",
                "image_id": self.image_id,
                "flavor_id": self.flavor_id,
            })

        c1 = vdu.connection_points.add()
        c1.name = str(uuid.uuid4())
        c1.virtual_link_id = self.virtual_link_id

        return vdu

    def virtual_link(self):
        """vlink config for Openstack

        Returns:
            VirtualLinkReqParams
        """
        vlink = RwcalYang.VirtualLinkReqParams.from_dict({
                    "name": str(uuid.uuid4()),
                    "subnet": '192.168.1.0/24',
            })

        return vlink


class Openstack(CloudConfig):
    def __init__(self, option):
        """
        Args:
            option (OptionParser)
        """
        self.image_id = None
        self.virtual_link_id = None
        self.flavor_id = None
        self.vdu_id = None

        super().__init__(self._cal(), self._account(option))

    def _cal(self):
        """
        Loads rw.cal plugin via libpeas
        """
        plugin = rw_peas.PeasPlugin('rwcal_openstack', 'RwCal-1.0')
        engine, info, extension = plugin()

        # Get the RwLogger context
        rwloggerctx = rwlogger.RwLog.Ctx.new("Cal-Log")

        cal = plugin.get_interface("Cloud")
        try:
            rc = cal.init(rwloggerctx)
            assert rc == RwStatus.SUCCESS
        except:
            logger.error("ERROR:Cal plugin instantiation failed. Aborting tests")
        else:
            logger.info("Openstack Cal plugin successfully instantiated")
        return cal

    def _account(self, option):
        """Cloud account information for Account

        Returns:
            CloudAccount
        """
        acct = RwcalYang.CloudAccount.from_dict({
            "account_type": "openstack",
            "openstack": {
                    "key": option.os_user,
                    "secret": option.os_password,
                    "auth_url": 'http://{}:5000/v3/'.format(option.os_host),
                    "tenant": option.os_tenant,
                    "mgmt_network": option.os_network
                }
            })

        return acct
    
    def _md5(self, fname, blksize=1048576):
        hash_md5 = hashlib.md5()
        with open(fname, "rb") as f:
            for chunk in iter(lambda: f.read(blksize), b""):
                hash_md5.update(chunk)
        return hash_md5.hexdigest()

    def image(self):
        """Provides Image config for openstack.

        Returns:
            ImageInfoItem
        """
        image = RwcalYang.ImageInfoItem.from_dict({
                "name": str(uuid.uuid4()),
                "location": os.path.join(os.getenv("RIFT_ROOT"), "images/rift-root-latest.qcow2"),
                "disk_format": "qcow2",
                "container_format": "bare",
                "checksum": self._md5(os.path.join(os.getenv("RIFT_ROOT"), "images/rift-root-latest.qcow2")),
            })
        return image

    def flavor(self):
        """Flavor config for openstack

        Returns:
            FlavorInfoItem
        """
        flavor = RwcalYang.FlavorInfoItem.from_dict({
                "name": str(uuid.uuid4()),
                "vm_flavor": {
                        "memory_mb": 16392,
                        "vcpu_count": 4,
                        "storage_gb": 40
                },
                "guest_epa": {
                        "cpu_pinning_policy": "DEDICATED",
                        "cpu_thread_pinning_policy": "SEPARATE",
                }})

        numa_node_count = 2
        flavor.guest_epa.numa_node_policy.node_cnt = numa_node_count
        for i in range(numa_node_count):
            node = flavor.guest_epa.numa_node_policy.node.add()
            node.id = i
            if i == 0:
                node.vcpu = [0, 1]
            elif i == 1:
                node.vcpu = [2, 3]
            node.memory_mb = 8196

        dev = flavor.guest_epa.pcie_device.add()
        dev.device_id = "PCI_10G_ALIAS"
        dev.count = 1

        return flavor

    def vdu(self):
        """Returns VDU config

        Returns:
            VDUInitParams
        """
        vdu = RwcalYang.VDUInitParams.from_dict({
                "name": str(uuid.uuid4()),
                "node_id": "123456789012345",
                "image_id": self.image_id,
                "flavor_id": self.flavor_id,
            })

        c1 = vdu.connection_points.add()
        c1.name = str(uuid.uuid4())
        c1.virtual_link_id = self.virtual_link_id

        return vdu

    def virtual_link(self):
        """vlink config for Openstack

        Returns:
            VirtualLinkReqParams
        """
        vlink = RwcalYang.VirtualLinkReqParams.from_dict({
                    "name": str(uuid.uuid4()),
                    "subnet": '192.168.1.0/24',
            })

        return vlink


@pytest.fixture(scope="module", params=[Openstack], ids=lambda val: val.__name__)
def cloud_config(request):
    return request.param(request.config.option)


@pytest.mark.incremental
class TestCalSetup:

    def test_start_server(self, cloud_config):
        cloud_config.start_server()

    def test_flavor_apis(self, cloud_config):
        """
        Asserts:
            1. If the new flavor is created and available via read APIs
            2. Verifies the READ APIs
        """
        account = cloud_config.account
        cal = cloud_config.cal

        status, new_flavor_id = cal.create_flavor(account, cloud_config.flavor())
        cloud_config.flavor_id = new_flavor_id
        assert status == RwStatus.SUCCESS

        status, flavors = cal.get_flavor_list(account)
        assert status == RwStatus.SUCCESS

        ids = []
        for flavor in flavors.flavorinfo_list:
            status, flavor_single = cal.get_flavor(account, flavor.id)
            assert status == RwStatus.SUCCESS
            assert flavor.id == flavor_single.id
            ids.append(flavor.id)

        assert new_flavor_id in ids

    def test_image_apis(self, cloud_config):
        """
        Asserts:
            1. If the new image is created and available via read APIs
            2. Verifies the READ APIs
        """
        account = cloud_config.account
        cal = cloud_config.cal

        if type(cloud_config) is Aws:
            # Hack!
            new_image_id = "ami-7070231a"
        else:
            status, new_image_id = cal.create_image(account, cloud_config.image())
            cloud_config.image_id = new_image_id
            assert status == RwStatus.SUCCESS
            cloud_config.check_state(new_image_id, "get_image", "active")


        status, images = cal.get_image_list(account)

        ids = []
        for image in images.imageinfo_list:
            status, image_single = cal.get_image(account, image.id)
            assert status == RwStatus.SUCCESS
            assert image_single.id == image.id
            ids.append(image.id)

        assert new_image_id in ids

    def test_virtual_link_create(self, cloud_config):
        """
        Asserts:
            1. If the new Vlink is created and available via read APIs
            2. Verifies the READ APIs
        """
        account = cloud_config.account
        cal = cloud_config.cal

        status, new_vlink_id = cal.create_virtual_link(account, cloud_config.virtual_link())
        cloud_config.virtual_link_id = new_vlink_id
        assert status.status == RwStatus.SUCCESS
        cloud_config.check_state(new_vlink_id, "get_virtual_link", "active")

        status, vlinks = cal.get_virtual_link_list(account)
        assert status == RwStatus.SUCCESS

        ids = []
        for vlink in vlinks.virtual_link_info_list:
            status, vlink_single = cal.get_virtual_link(account, vlink.virtual_link_id)
            assert status == RwStatus.SUCCESS
            assert vlink_single.virtual_link_id == vlink.virtual_link_id
            ids.append(vlink.virtual_link_id)

        assert new_vlink_id in ids

    def test_vdu_apis(self, cloud_config):
        """
        Asserts:
            1. If the new VDU is created and available via read APIs
            2. Verifies the READ APIs
        """
        account = cloud_config.account
        cal = cloud_config.cal

        status, new_vdu_id = cal.create_vdu(account, cloud_config.vdu())
        cloud_config.vdu_id = new_vdu_id
        assert status.status == RwStatus.SUCCESS
        cloud_config.check_state(new_vdu_id, "get_vdu", "active")

        status, vdus = cal.get_vdu_list(account)
        assert status == RwStatus.SUCCESS

        ids = []
        for vdu in vdus.vdu_info_list:
            status, vdu_single = cal.get_vdu(account, vdu.vdu_id)
            assert status == RwStatus.SUCCESS
            assert vdu_single.vdu_id == vdu.vdu_id
            ids.append(vdu.vdu_id)

        assert new_vdu_id in ids

    def test_modify_vdu_api(self, cloud_config):
        account = cloud_config.account
        cal = cloud_config.cal

        vdu_modify = RwcalYang.VDUModifyParams()
        vdu_modify.vdu_id = cloud_config.vdu_id
        c1 = vdu_modify.connection_points_add.add()
        c1.name = "c_modify1"
        # Set the new vlink
        c1.virtual_link_id = cloud_config.virtual_link_id

        status = cal.modify_vdu(account, vdu_modify)
        assert status == RwStatus.SUCCESS

@pytest.mark.incremental
class TestCalTeardown:
    def test_flavor_delete(self, cloud_config):
        """
        Asserts:
            1. If flavor is deleted
        """
        account = cloud_config.account
        cal = cloud_config.cal

        if type(cloud_config) != Aws:
            status = cal.delete_flavor(account, cloud_config.flavor_id)
            assert status == RwStatus.SUCCESS

    def test_image_delete(self, cloud_config):
        """
        Asserts:
            1. If image is deleted
        """
        account = cloud_config.account
        cal = cloud_config.cal

        if type(cloud_config) != Aws:
            status = cal.delete_image(account, cloud_config.image_id)
            assert status == RwStatus.SUCCESS

    def test_virtual_link_delete(self, cloud_config):
        """
        Asserts:
            1. If VLink is deleted
        """
        account = cloud_config.account
        cal = cloud_config.cal

        status = cal.delete_virtual_link(account, cloud_config.virtual_link_id)
        assert status == RwStatus.SUCCESS

    def test_delete_vdu(self, cloud_config):
        """
        Asserts:
            1. If VDU is deleted
        """
        account = cloud_config.account
        cal = cloud_config.cal

        status = cal.delete_vdu(account, cloud_config.vdu_id)
        assert status == RwStatus.SUCCESS

    def test_stop_server(self, cloud_config):
        cloud_config.stop_server()
