# Copyright 2015 Hewlett-Packard Development Company, L.P.
#
# 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.

import six

import testscenarios

from nova import context
from nova import db
from nova import exception as ex
from nova import objects
from nova import test
from nova.tests import fixtures as nova_fixtures
from nova.tests.functional import integrated_helpers as helper
from nova.tests.unit import policy_fixture


def rand_flavor(**kwargs):
    flav = {
        'name': 'name-%s' % helper.generate_random_alphanumeric(10),
        'id': helper.generate_random_alphanumeric(10),
        'ram': int(helper.generate_random_numeric(2)) + 1,
        'disk': int(helper.generate_random_numeric(3)),
        'vcpus': int(helper.generate_random_numeric(1)) + 1,
    }
    flav.update(kwargs)
    return flav


class FlavorManageFullstack(testscenarios.WithScenarios, test.TestCase):
    """Tests for flavors manage administrative command.

    Extension: os-flavors-manage

    os-flavors-manage adds a set of admin functions to the flavors
    resource for the creation and deletion of flavors.

    POST /v2/flavors:

    ::

        {
            'name': NAME, # string, required unique
            'id': ID, # string, required unique
            'ram': RAM, # in MB, required
            'vcpus': VCPUS, # int value, required
            'disk': DISK, # in GB, required
            'OS-FLV-EXT-DATA:ephemeral', # in GB, ephemeral disk size
            'is_public': IS_PUBLIC, # boolean
            'swap': SWAP, # in GB?
            'rxtx_factor': RXTX, # ???
        }

    Returns Flavor

    DELETE /v2/flavors/ID


    Functional Test Scope:

    This test starts the wsgi stack for the nova api services, uses an
    in memory database to ensure the path through the wsgi layer to
    the database.

    """

    _additional_fixtures = []

    scenarios = [
        # test v2.1 base microversion
        ('v2_1', {
            'api_major_version': 'v2.1'}),
    ]

    def setUp(self):
        super(FlavorManageFullstack, self).setUp()

        # load any additional fixtures specified by the scenario
        for fix in self._additional_fixtures:
            self.useFixture(fix())

        self.useFixture(policy_fixture.RealPolicyFixture())
        api_fixture = self.useFixture(
            nova_fixtures.OSAPIFixture(
                api_version=self.api_major_version))

        # NOTE(sdague): because this test is primarily an admin API
        # test default self.api to the admin api.
        self.api = api_fixture.admin_api
        self.user_api = api_fixture.api

    def assertFlavorDbEqual(self, flav, flavdb):
        # a mapping of the REST params to the db fields
        mapping = {
            'name': 'name',
            'disk': 'root_gb',
            'ram': 'memory_mb',
            'vcpus': 'vcpus',
            'id': 'flavorid',
            'swap': 'swap'
        }
        for k, v in six.iteritems(mapping):
            if k in flav:
                self.assertEqual(flav[k], flavdb[v],
                                 "%s != %s" % (flav, flavdb))

    def assertFlavorAPIEqual(self, flav, flavapi):
        # for all keys in the flavor, ensure they are correctly set in
        # flavapi response.
        for k, v in six.iteritems(flav):
            if k in flavapi:
                self.assertEqual(flav[k], flavapi[k],
                                 "%s != %s" % (flav, flavapi))
            else:
                self.fail("Missing key: %s in flavor: %s" % (k, flavapi))

    def assertFlavorInList(self, flav, flavlist):
        for item in flavlist['flavors']:
            if flav['id'] == item['id']:
                self.assertEqual(flav['name'], item['name'])
                return
        self.fail("%s not found in %s" % (flav, flavlist))

    def assertFlavorNotInList(self, flav, flavlist):
        for item in flavlist['flavors']:
            if flav['id'] == item['id']:
                self.fail("%s found in %s" % (flav, flavlist))

    def test_flavor_manage_func_negative(self):
        """Test flavor manage edge conditions.

        - Bogus body is a 400
        - Unknown flavor is a 404
        - Deleting unknown flavor is a 404
        """
        # Test for various API failure conditions
        # bad body is 400
        resp = self.api.api_post('flavors', '', check_response_status=False)
        self.assertEqual(400, resp.status)

        # get unknown flavor is 404
        resp = self.api.api_delete('flavors/foo', check_response_status=False)
        self.assertEqual(404, resp.status)

        # delete unknown flavor is 404
        resp = self.api.api_delete('flavors/foo', check_response_status=False)
        self.assertEqual(404, resp.status)

        ctx = context.get_admin_context()
        # bounds conditions - invalid vcpus
        flav = {'flavor': rand_flavor(vcpus=0)}
        resp = self.api.api_post('flavors', flav, check_response_status=False)
        self.assertEqual(400, resp.status, resp)
        # ... and ensure that we didn't leak it into the db
        self.assertRaises(ex.FlavorNotFound,
                          objects.Flavor.get_by_flavor_id,
                          ctx, flav['flavor']['id'])

        # bounds conditions - invalid ram
        flav = {'flavor': rand_flavor(ram=0)}

        resp = self.api.api_post('flavors', flav, check_response_status=False)
        self.assertEqual(400, resp.status)
        # ... and ensure that we didn't leak it into the db
        self.assertRaises(ex.FlavorNotFound,
                          objects.Flavor.get_by_flavor_id,
                          ctx, flav['flavor']['id'])

        # NOTE(sdague): if there are other bounds conditions that
        # should be checked, stack them up here.

    def test_flavor_manage_deleted(self):
        """Ensure the behavior around a deleted flavor is stable.

        - Fetching a deleted flavor works, and returns the flavor info.
        - Listings should not contain deleted flavors

        """
        # create a deleted flavor
        new_flav = {'flavor': rand_flavor()}
        self.api.api_post('flavors', new_flav)
        self.api.api_delete('flavors/%s' % new_flav['flavor']['id'])

        # deleted flavor should not show up in a list
        resp = self.api.api_get('flavors')
        self.assertFlavorNotInList(new_flav['flavor'], resp.body)

    def test_flavor_create_frozen(self):
        ctx = context.get_admin_context()
        db.flavor_create(ctx, {
            'name': 'foo', 'memory_mb': 512, 'vcpus': 1,
            'root_gb': 1, 'ephemeral_gb': 0, 'flavorid': 'foo',
            'swap': 0, 'rxtx_factor': 1.0, 'vcpu_weight': 1,
            'disabled': False, 'is_public': True,
        })
        new_flav = {'flavor': rand_flavor()}
        resp = self.api.api_post('flavors', new_flav,
                                 check_response_status=False)
        self.assertEqual(409, resp.status)

    def test_flavor_manage_func(self):
        """Basic flavor creation lifecycle testing.

        - Creating a flavor
        - Ensure it's in the database
        - Ensure it's in the listing
        - Delete it
        - Ensure it's hidden in the database
        """

        ctx = context.get_admin_context()
        flav1 = {
            'flavor': rand_flavor(),
         }

        # Create flavor and ensure it made it to the database
        self.api.api_post('flavors', flav1)

        flav1db = objects.Flavor.get_by_flavor_id(ctx, flav1['flavor']['id'])
        self.assertFlavorDbEqual(flav1['flavor'], flav1db)

        # Ensure new flavor is seen in the listing
        resp = self.api.api_get('flavors')
        self.assertFlavorInList(flav1['flavor'], resp.body)

        # Delete flavor and ensure it was removed from the database
        self.api.api_delete('flavors/%s' % flav1['flavor']['id'])
        self.assertRaises(ex.FlavorNotFound,
                          objects.Flavor.get_by_flavor_id,
                          ctx, flav1['flavor']['id'])

        resp = self.api.api_delete('flavors/%s' % flav1['flavor']['id'],
                                   check_response_status=False)
        self.assertEqual(404, resp.status)

    def test_flavor_manage_permissions(self):
        """Ensure that regular users can't create or delete flavors.

        """
        ctx = context.get_admin_context()
        flav1 = {'flavor': rand_flavor()}

        # Ensure user can't create flavor
        resp = self.user_api.api_post('flavors', flav1,
                                      check_response_status=False)
        self.assertEqual(403, resp.status)
        # ... and that it didn't leak through
        self.assertRaises(ex.FlavorNotFound,
                        objects.Flavor.get_by_flavor_id,
                        ctx, flav1['flavor']['id'])

        # Create the flavor as the admin user
        self.api.api_post('flavors', flav1)

        # Ensure user can't delete flavors from our cloud
        resp = self.user_api.api_delete('flavors/%s' % flav1['flavor']['id'],
                                        check_response_status=False)
        self.assertEqual(403, resp.status)
        # ... and ensure that we didn't actually delete the flavor,
        # this will throw an exception if we did.
        objects.Flavor.get_by_flavor_id(ctx, flav1['flavor']['id'])
