# Copyright 2014 Rackspace Hosting
# All Rights Reserved.
#
#    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.
"""Ironic object test utilities."""

import functools
import inspect

from ironic.common import exception
from ironic.common.i18n import _
from ironic import objects
from ironic.objects import notification
from ironic.tests.unit.db import utils as db_utils


def check_keyword_arguments(func):
    @functools.wraps(func)
    def wrapper(**kw):
        obj_type = kw.pop('object_type')
        result = func(**kw)

        extra_args = set(kw) - set(result)
        if extra_args:
            raise exception.InvalidParameterValue(
                _("Unknown keyword arguments (%(extra)s) were passed "
                  "while creating a test %(object_type)s object.") %
                {"extra": ", ".join(extra_args),
                 "object_type": obj_type})

        return result

    return wrapper


def get_test_node(ctxt, **kw):
    """Return a Node object with appropriate attributes.

    NOTE: The object leaves the attributes marked as changed, such
    that a create() could be used to commit it to the DB.
    """
    kw['object_type'] = 'node'
    get_db_node_checked = check_keyword_arguments(db_utils.get_test_node)
    db_node = get_db_node_checked(**kw)

    # Let DB generate ID if it isn't specified explicitly
    if 'id' not in kw:
        del db_node['id']
    node = objects.Node(ctxt)
    for key in db_node:
        if key == 'traits':
            # convert list of strings to object
            raw_traits = db_node['traits']
            trait_list = []
            for raw_trait in raw_traits:
                trait = objects.Trait(ctxt, trait=raw_trait)
                trait_list.append(trait)
            node.traits = objects.TraitList(ctxt, objects=trait_list)
            node.traits.obj_reset_changes()
        else:
            setattr(node, key, db_node[key])
    return node


def create_test_node(ctxt, **kw):
    """Create and return a test node object.

    Create a node in the DB and return a Node object with appropriate
    attributes.
    """
    node = get_test_node(ctxt, **kw)
    node.create()
    return node


def get_test_port(ctxt, **kw):
    """Return a Port object with appropriate attributes.

    NOTE: The object leaves the attributes marked as changed, such
    that a create() could be used to commit it to the DB.
    """
    kw['object_type'] = 'port'
    get_db_port_checked = check_keyword_arguments(
        db_utils.get_test_port)
    db_port = get_db_port_checked(**kw)

    # Let DB generate ID if it isn't specified explicitly
    if 'id' not in kw:
        del db_port['id']
    port = objects.Port(ctxt)
    for key in db_port:
        setattr(port, key, db_port[key])
    return port


def create_test_port(ctxt, **kw):
    """Create and return a test port object.

    Create a port in the DB and return a Port object with appropriate
    attributes.
    """
    port = get_test_port(ctxt, **kw)
    port.create()
    return port


def get_test_chassis(ctxt, **kw):
    """Return a Chassis object with appropriate attributes.

    NOTE: The object leaves the attributes marked as changed, such
    that a create() could be used to commit it to the DB.
    """
    kw['object_type'] = 'chassis'
    get_db_chassis_checked = check_keyword_arguments(
        db_utils.get_test_chassis)
    db_chassis = get_db_chassis_checked(**kw)

    # Let DB generate ID if it isn't specified explicitly
    if 'id' not in kw:
        del db_chassis['id']
    chassis = objects.Chassis(ctxt)
    for key in db_chassis:
        setattr(chassis, key, db_chassis[key])
    return chassis


def create_test_chassis(ctxt, **kw):
    """Create and return a test chassis object.

    Create a chassis in the DB and return a Chassis object with appropriate
    attributes.
    """
    chassis = get_test_chassis(ctxt, **kw)
    chassis.create()
    return chassis


def get_test_portgroup(ctxt, **kw):
    """Return a Portgroup object with appropriate attributes.

    NOTE: The object leaves the attributes marked as changed, such
    that a create() could be used to commit it to the DB.
    """
    kw['object_type'] = 'portgroup'
    get_db_port_group_checked = check_keyword_arguments(
        db_utils.get_test_portgroup)
    db_portgroup = get_db_port_group_checked(**kw)

    # Let DB generate ID if it isn't specified explicitly
    if 'id' not in kw:
        del db_portgroup['id']
    portgroup = objects.Portgroup(ctxt)
    for key in db_portgroup:
        setattr(portgroup, key, db_portgroup[key])
    return portgroup


def create_test_portgroup(ctxt, **kw):
    """Create and return a test portgroup object.

    Create a portgroup in the DB and return a Portgroup object with appropriate
    attributes.
    """
    portgroup = get_test_portgroup(ctxt, **kw)
    portgroup.create()
    return portgroup


def get_test_volume_connector(ctxt, **kw):
    """Return a VolumeConnector object with appropriate attributes.

    NOTE: The object leaves the attributes marked as changed, such
    that a create() could be used to commit it to the DB.
    """
    db_volume_connector = db_utils.get_test_volume_connector(**kw)
    # Let DB generate ID if it isn't specified explicitly
    if 'id' not in kw:
        del db_volume_connector['id']
    volume_connector = objects.VolumeConnector(ctxt)
    for key in db_volume_connector:
        setattr(volume_connector, key, db_volume_connector[key])
    return volume_connector


def create_test_volume_connector(ctxt, **kw):
    """Create and return a test volume connector object.

    Create a volume connector in the DB and return a VolumeConnector object
    with appropriate attributes.
    """
    volume_connector = get_test_volume_connector(ctxt, **kw)
    volume_connector.create()
    return volume_connector


def get_test_volume_target(ctxt, **kw):
    """Return a VolumeTarget object with appropriate attributes.

    NOTE: The object leaves the attributes marked as changed, such
    that a create() could be used to commit it to the DB.
    """
    db_volume_target = db_utils.get_test_volume_target(**kw)
    # Let DB generate ID if it isn't specified explicitly
    if 'id' not in kw:
        del db_volume_target['id']
    volume_target = objects.VolumeTarget(ctxt)
    for key in db_volume_target:
        setattr(volume_target, key, db_volume_target[key])
    return volume_target


def create_test_volume_target(ctxt, **kw):
    """Create and return a test volume target object.

    Create a volume target in the DB and return a VolumeTarget object with
    appropriate attributes.
    """
    volume_target = get_test_volume_target(ctxt, **kw)
    volume_target.create()
    return volume_target


def get_test_bios_setting(ctxt, **kw):
    """Return a BiosSettingList object with appropriate attributes.

    NOTE: The object leaves the attributes marked as changed, such
    that a create() could be used to commit it to the DB.
    """
    kw['object_type'] = 'bios'
    db_bios_setting = db_utils.get_test_bios_setting(**kw)
    bios_setting = objects.BIOSSetting(ctxt)
    for key in db_bios_setting:
        setattr(bios_setting, key, db_bios_setting[key])
    return bios_setting


def create_test_bios_setting(ctxt, **kw):
    """Create and return a test bios setting list object.

    Create a BIOS setting list in the DB and return a BIOSSettingList
    object with appropriate attributes.
    """
    bios_setting = get_test_bios_setting(ctxt, **kw)
    bios_setting.create()
    return bios_setting


def create_test_conductor(ctxt, **kw):
    """Register and return a test conductor object."""
    args = db_utils.get_test_conductor(**kw)
    conductor = objects.Conductor.register(ctxt, args['hostname'],
                                           args['drivers'],
                                           args['conductor_group'],
                                           update_existing=True)
    return conductor


def get_test_allocation(ctxt, **kw):
    """Return an Allocation object with appropriate attributes.

    NOTE: The object leaves the attributes marked as changed, such
    that a create() could be used to commit it to the DB.
    """
    kw['object_type'] = 'allocation'
    get_db_allocation_checked = check_keyword_arguments(
        db_utils.get_test_allocation)
    db_allocation = get_db_allocation_checked(**kw)

    # Let DB generate ID if it isn't specified explicitly
    if 'id' not in kw:
        del db_allocation['id']
    allocation = objects.Allocation(ctxt)
    for key in db_allocation:
        setattr(allocation, key, db_allocation[key])
    return allocation


def create_test_allocation(ctxt, **kw):
    """Create and return a test allocation object.

    Create an allocation in the DB and return an Allocation object with
    appropriate attributes.
    """
    allocation = get_test_allocation(ctxt, **kw)
    allocation.create()
    return allocation


def get_test_deploy_template(ctxt, **kw):
    """Return a DeployTemplate object with appropriate attributes.

    NOTE: The object leaves the attributes marked as changed, such
    that a create() could be used to commit it to the DB.
    """
    db_template = db_utils.get_test_deploy_template(**kw)
    # Let DB generate ID if it isn't specified explicitly
    if 'id' not in kw:
        del db_template['id']
    if 'steps' not in kw:
        for step in db_template['steps']:
            del step['id']
            del step['deploy_template_id']
    else:
        for kw_step, template_step in zip(kw['steps'], db_template['steps']):
            if 'id' not in kw_step and 'id' in template_step:
                del template_step['id']
    template = objects.DeployTemplate(ctxt)
    for key in db_template:
        setattr(template, key, db_template[key])
    return template


def create_test_deploy_template(ctxt, **kw):
    """Create and return a test deploy template object.

    NOTE: The object leaves the attributes marked as changed, such
    that a create() could be used to commit it to the DB.
    """
    template = get_test_deploy_template(ctxt, **kw)
    template.create()
    return template


def get_payloads_with_schemas(from_module):
    """Get the Payload classes with SCHEMAs defined.

    :param from_module: module from which to get the classes.
    :returns: list of Payload classes that have SCHEMAs defined.

    """
    payloads = []
    for name, payload in inspect.getmembers(from_module, inspect.isclass):
        # Assume that Payload class names end in 'Payload'.
        if name.endswith("Payload"):
            base_classes = inspect.getmro(payload)
            if notification.NotificationPayloadBase not in base_classes:
                # The class may have the desired name but it isn't a REAL
                # Payload class; skip it.
                continue

            # First class is this payload class, parent class is the 2nd
            # one in the tuple
            parent = base_classes[1]
            if (not hasattr(parent, 'SCHEMA')
                or parent.SCHEMA != payload.SCHEMA):
                payloads.append(payload)

    return payloads


def get_test_runbook(ctxt, **kw):
    """Return a Runbook object with appropriate attributes.

    NOTE: The object leaves the attributes marked as changed, such
    that a create() could be used to commit it to the DB.
    """
    db_runbook = db_utils.get_test_runbook(**kw)
    # Let DB generate ID if it isn't specified explicitly
    if 'id' not in kw:
        del db_runbook['id']
    if 'steps' not in kw:
        for step in db_runbook['steps']:
            del step['id']
            del step['runbook_id']
    else:
        for kw_step, runbook_step in zip(kw['steps'], db_runbook['steps']):
            if 'id' not in kw_step and 'id' in runbook_step:
                del runbook_step['id']
    runbook = objects.Runbook(ctxt)
    for key in db_runbook:
        setattr(runbook, key, db_runbook[key])
    return runbook


def create_test_runbook(ctxt, **kw):
    """Create and return a test runbook object.

    NOTE: The object leaves the attributes marked as changed, such
    that a create() could be used to commit it to the DB.
    """
    runbook = get_test_runbook(ctxt, **kw)
    runbook.create()
    return runbook


class SchemasTestMixIn(object):
    def _check_payload_schemas(self, from_module, fields):
        """Assert that the Payload SCHEMAs have the expected properties.

           A payload's SCHEMA should:

           1. Have each of its keys in the payload's fields
           2. Have each member of the schema match with a corresponding field
           in the object
        """
        resource = from_module.__name__.split('.')[-1]
        payloads = get_payloads_with_schemas(from_module)
        for payload in payloads:
            for schema_key in payload.SCHEMA:
                self.assertIn(schema_key, payload.fields,
                              "for %s, schema key %s is not in fields"
                              % (payload, schema_key))
                key = payload.SCHEMA[schema_key][1]
                self.assertIn(key, fields,
                              "for %s, schema key %s has invalid %s "
                              "field %s" % (payload, schema_key, resource,
                                            key))


def create_test_inventory(ctxt, node, **kw):
    """Create and return a test node inventory object."""
    inv = objects.NodeInventory(ctxt)
    if not isinstance(node, str):
        node = node.id
    kw['node_id'] = node
    for key, value in kw.items():
        setattr(inv, key, value)
    inv.create()
    return inv


def get_test_firmware_component(ctxt, **kw):
    """Return a FirmwareComponent object with appropriate attributes.

    NOTE: The object leaves the attributes marked as changed, such
    that a create() could be used to commit it to the DB.
    """
    kw['object_type'] = 'firmware'
    get_db_fw_checked = check_keyword_arguments(
        db_utils.get_test_firmware_component)
    db_fw_cmp = get_db_fw_checked(**kw)

    if 'id' not in kw:
        del db_fw_cmp['id']

    fw_cmp = objects.FirmwareComponent(ctxt)
    for key in db_fw_cmp:
        setattr(fw_cmp, key, db_fw_cmp[key])

    return fw_cmp


def create_test_firmware_component(ctxt, **kw):
    """Create and return a test Firmware Component object.

    Create a Firmware Component in the DB and return a FirmwareComponent
    object with appropriate attributes.
    """
    fw_cmp = get_test_firmware_component(ctxt, **kw)
    fw_cmp.create()
    return fw_cmp


def get_test_inspection_rule(ctxt, **kw):
    """Return an InpsectionRule object with appropriate attributes.

    NOTE: The object leaves the attributes marked as changed, such
    that a create() could be used to commit it to the DB.
    """
    db_inspection_rule = db_utils.get_test_inspection_rule(**kw)
    inspection_rule = objects.InspectionRule(ctxt)
    for key in db_inspection_rule:
        setattr(inspection_rule, key, db_inspection_rule[key])
    return inspection_rule


def create_test_inspection_rule(ctxt, **kw):
    """Create and return a test inspection rule object.

    NOTE: The object leaves the attributes marked as changed, such
    that a create() could be used to commit it to the DB.
    """
    inspection_rule = get_test_inspection_rule(ctxt, **kw)
    inspection_rule.create()
    return inspection_rule
