# Copyright 2011 OpenStack Foundation
# 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.

"""
Tests dealing with HTTP rate-limiting.
"""

from http import client as httplib
from io import StringIO
from unittest import mock

from oslo_limit import fixture as limit_fixture
from oslo_serialization import jsonutils
from oslo_utils import encodeutils

from nova.api.openstack.compute import limits as limits_v21
from nova.api.openstack.compute import views
from nova.api.openstack import wsgi
import nova.context
from nova import exception
from nova.limit import local as local_limit
from nova.limit import placement as placement_limit
from nova import objects
from nova.policies import limits as l_policies
from nova import test
from nova.tests.unit.api.openstack import fakes
from nova.tests.unit import matchers


def fake_get_project_quotas(context, project_id, usages=True):
    absolute_limits = {
        'cores': -1,
        'floating_ips': 10,
        'injected_files': -1,
        'injected_file_content_bytes': -1,
        'injected_file_path_bytes': -1,
        'instances': 5,
        'key_pairs': -1,
        'metadata_items': -1,
        'ram': 512,
        'server_groups': 10,
        'server_group_members': -1,
        'security_groups': 10,
        'security_group_rules': 20,
    }

    return {
        k: {'limit': v, 'in_use': max(-1, v // 2)}
        for k, v in absolute_limits.items()
    }


class BaseLimitTestSuite(test.NoDBTestCase):
    """Base test suite which provides relevant stubs and time abstraction."""

    def setUp(self):
        super().setUp()
        self.time = 0.0

        patcher_get_project_quotas = mock.patch.object(
            nova.quota.QUOTAS,
            "get_project_quotas",
            side_effect=fake_get_project_quotas)
        self.mock_get_project_quotas = patcher_get_project_quotas.start()
        self.addCleanup(patcher_get_project_quotas.stop)
        patcher = self.mock_can = mock.patch('nova.context.RequestContext.can')
        self.mock_can = patcher.start()
        self.addCleanup(patcher.stop)

    def _get_time(self):
        """Return the "time" according to this test suite."""
        return self.time


class LimitsControllerTestV21(BaseLimitTestSuite):
    """Tests for `limits.LimitsController` class."""
    limits_controller = limits_v21.LimitsController

    def setUp(self):
        """Run before each test."""
        super(LimitsControllerTestV21, self).setUp()
        self.controller = wsgi.Resource(self.limits_controller())
        self.ctrler = self.limits_controller()

    def _get_index_request(self, accept_header="application/json",
                           tenant_id=None, user_id='testuser',
                           project_id='testproject'):
        """Helper to set routing arguments."""
        request = fakes.HTTPRequest.blank('', version='2.1')
        if tenant_id:
            request = fakes.HTTPRequest.blank('/?tenant_id=%s' % tenant_id,
                                              version='2.1')

        request.accept = accept_header
        request.environ["wsgiorg.routing_args"] = (None, {
            "action": "index",
            "controller": "",
        })
        context = nova.context.RequestContext(user_id, project_id)
        request.environ["nova.context"] = context
        return request

    def test_index_json(self):
        self._test_index_json()

    def test_index_json_by_tenant(self):
        self._test_index_json('faketenant')

    def _test_index_json(self, tenant_id=None):
        # Test getting limit details in JSON.
        request = self._get_index_request(tenant_id=tenant_id)
        context = request.environ["nova.context"]
        if tenant_id is None:
            tenant_id = context.project_id

        expected = {
            "limits": {
                "absolute": {
                    "maxImageMeta": -1,
                    "maxPersonality": -1,
                    "maxPersonalitySize": -1,
                    "maxSecurityGroups": 10,
                    "maxSecurityGroupRules": 20,
                    "maxServerGroups": 10,
                    "maxServerGroupMembers": -1,
                    "maxServerMeta": -1,
                    "maxTotalCores": -1,
                    "maxTotalFloatingIps": 10,
                    "maxTotalInstances": 5,
                    "maxTotalKeypairs": -1,
                    "maxTotalRAMSize": 512,
                    "totalCoresUsed": -1,
                    "totalFloatingIpsUsed": 5,
                    "totalInstancesUsed": 2,
                    "totalRAMUsed": 256,
                    "totalSecurityGroupsUsed": 5,
                    "totalServerGroupsUsed": 5,
                },
                "rate": [],
            },
        }

        response = request.get_response(self.controller)

        body = jsonutils.loads(response.body)
        self.assertEqual(expected, body)
        self.mock_get_project_quotas.assert_called_once_with(
            context, tenant_id, usages=True)

    def test_admin_can_fetch_limits_for_a_given_tenant_id(self):
        project_id = "123456"
        user_id = "A1234"
        tenant_id = 'abcd'
        fake_req = self._get_index_request(tenant_id=tenant_id,
                                           user_id=user_id,
                                           project_id=project_id)
        context = fake_req.environ["nova.context"]
        self.mock_get_project_quotas.side_effect = None
        self.mock_get_project_quotas.return_value = {}

        fake_req.get_response(self.controller)
        self.assertEqual(2, self.mock_can.call_count)
        self.mock_can.assert_called_with(
            l_policies.OTHER_PROJECT_LIMIT_POLICY_NAME)
        self.mock_get_project_quotas.assert_called_once_with(
            context, tenant_id, usages=True)

    def _test_admin_can_fetch_used_limits_for_own_project(self, req_get):
        project_id = "123456"
        if 'tenant_id' in req_get:
            project_id = req_get['tenant_id']

        user_id = "A1234"
        fake_req = self._get_index_request(user_id=user_id,
                                           project_id=project_id)
        context = fake_req.environ["nova.context"]

        self.mock_get_project_quotas.side_effect = None
        self.mock_get_project_quotas.return_value = {}

        fake_req.get_response(self.controller)
        self.mock_get_project_quotas.assert_called_once_with(
            context, project_id, usages=True)

    def test_admin_can_fetch_used_limits_for_own_project(self):
        req_get = {}
        self._test_admin_can_fetch_used_limits_for_own_project(req_get)

    def test_admin_can_fetch_used_limits_for_dummy_only(self):
        # for back compatible we allow additional param to be send to req.GET
        # it can be removed when we add restrictions to query param later
        req_get = {'dummy': 'dummy'}
        self._test_admin_can_fetch_used_limits_for_own_project(req_get)

    def test_admin_can_fetch_used_limits_with_positive_int(self):
        req_get = {'tenant_id': 123}
        self._test_admin_can_fetch_used_limits_for_own_project(req_get)

    def test_admin_can_fetch_used_limits_with_negative_int(self):
        req_get = {'tenant_id': -1}
        self._test_admin_can_fetch_used_limits_for_own_project(req_get)

    def test_admin_can_fetch_used_limits_with_unknown_param(self):
        req_get = {'tenant_id': '123', 'unknown': 'unknown'}
        self._test_admin_can_fetch_used_limits_for_own_project(req_get)

    def test_used_limits_fetched_for_context_project_id(self):
        project_id = "123456"
        fake_req = self._get_index_request(project_id=project_id)
        context = fake_req.environ["nova.context"]
        self.mock_get_project_quotas.side_effect = None
        self.mock_get_project_quotas.return_value = {}

        fake_req.get_response(self.controller)

        self.mock_get_project_quotas.assert_called_once_with(
            context, project_id, usages=True)


class FakeHttplibSocket(object):
    """Fake `httplib.HTTPResponse` replacement."""

    def __init__(self, response_string):
        """Initialize new `FakeHttplibSocket`."""
        self._buffer = StringIO(response_string)

    def makefile(self, _mode, _other):
        """Returns the socket's internal buffer."""
        return self._buffer


class FakeHttplibConnection(object):
    """Fake `httplib.HTTPConnection`."""

    def __init__(self, app, host):
        """Initialize `FakeHttplibConnection`."""
        self.app = app
        self.host = host

    def request(self, method, path, body="", headers=None):
        """Requests made via this connection actually get translated and routed
        into our WSGI app, we then wait for the response and turn it back into
        an `httplib.HTTPResponse`.
        """
        if not headers:
            headers = {}

        req = fakes.HTTPRequest.blank(path)
        req.method = method
        req.headers = headers
        req.host = self.host
        req.body = encodeutils.safe_encode(body)

        resp = str(req.get_response(self.app))
        resp = "HTTP/1.0 %s" % resp
        sock = FakeHttplibSocket(resp)
        self.http_response = httplib.HTTPResponse(sock)
        self.http_response.begin()

    def getresponse(self):
        """Return our generated response from the request."""
        return self.http_response


class LimitsViewBuilderTest(test.NoDBTestCase):
    def setUp(self):
        super(LimitsViewBuilderTest, self).setUp()
        self.view_builder = views.limits.ViewBuilder()
        self.req = fakes.HTTPRequest.blank('/?tenant_id=None')

    def test_build_limits(self):
        quotas = {
            "metadata_items": {'limit': 1, 'in_use': 1},
            "injected_files": {'limit': 5, 'in_use': 1},
            "injected_file_content_bytes": {'limit': 5, 'in_use': 1},
        }
        expected_limits = {
            "limits": {
                "rate": [],
                "absolute": {
                    "maxServerMeta": 1,
                    "maxImageMeta": 1,
                    "maxPersonality": 5,
                    "maxPersonalitySize": 5,
                }
            },
        }

        output = self.view_builder.build(self.req, quotas)
        self.assertThat(output, matchers.DictMatches(expected_limits))

    def test_build_limits_empty_limits(self):
        quotas = {}
        expected_limits = {"limits": {"rate": [], "absolute": {}}}

        output = self.view_builder.build(self.req, quotas)
        self.assertThat(output, matchers.DictMatches(expected_limits))


class LimitsControllerTestV236(BaseLimitTestSuite):

    def setUp(self):
        super(LimitsControllerTestV236, self).setUp()
        self.controller = limits_v21.LimitsController()
        self.req = fakes.HTTPRequest.blank("/?tenant_id=faketenant",
                                           version='2.36')

    def test_index_filtered(self):
        response = self.controller.index(self.req)
        expected_response = {
            "limits": {
                "rate": [],
                "absolute": {
                    "maxImageMeta": -1,
                    "maxPersonality": -1,
                    "maxPersonalitySize": -1,
                    "maxServerGroups": 10,
                    "maxServerGroupMembers": -1,
                    "maxServerMeta": -1,
                    "maxTotalCores": -1,
                    "maxTotalInstances": 5,
                    "maxTotalKeypairs": -1,
                    "maxTotalRAMSize": 512,
                    "totalCoresUsed": -1,
                    "totalInstancesUsed": 2,
                    "totalRAMUsed": 256,
                    "totalServerGroupsUsed": 5,
                },
            },
        }
        self.assertEqual(expected_response, response)


class LimitsControllerTestV239(BaseLimitTestSuite):

    def setUp(self):
        super(LimitsControllerTestV239, self).setUp()
        self.controller = limits_v21.LimitsController()
        self.req = fakes.HTTPRequest.blank("/?tenant_id=faketenant",
                                           version='2.39')

    def test_index_filtered_no_max_image_meta(self):
        response = self.controller.index(self.req)
        # starting from version 2.39 there is no 'maxImageMeta' field
        # in response after removing 'image-metadata' proxy API
        self.assertNotIn('maxImageMeta', response['limits']['absolute'])
        self.assertIn('maxServerMeta', response['limits']['absolute'])


class LimitsControllerTestV275(BaseLimitTestSuite):
    def setUp(self):
        super(LimitsControllerTestV275, self).setUp()
        self.controller = limits_v21.LimitsController()

    def test_index_additional_query_param_old_version(self):
        req = fakes.HTTPRequest.blank("/?unknown=fake", version='2.74')
        self.controller.index(req)
        self.controller.index(req)

    def test_index_additional_query_param(self):
        req = fakes.HTTPRequest.blank("/?unknown=fake", version='2.75')
        self.assertRaises(
            exception.ValidationError,
            self.controller.index, req=req)


class NoopLimitsControllerTest(test.NoDBTestCase):
    quota_driver = "nova.quota.NoopQuotaDriver"

    def setUp(self):
        super(NoopLimitsControllerTest, self).setUp()
        self.flags(driver=self.quota_driver, group="quota")
        self.controller = limits_v21.LimitsController()
        # remove policy checks
        patcher = self.mock_can = mock.patch('nova.context.RequestContext.can')
        self.mock_can = patcher.start()
        self.addCleanup(patcher.stop)

    def test_index_v21(self):
        req = fakes.HTTPRequest.blank("/")
        response = self.controller.index(req)
        expected_response = {
            "limits": {
                "rate": [],
                "absolute": {
                    'maxImageMeta': -1,
                    'maxPersonality': -1,
                    'maxPersonalitySize': -1,
                    'maxSecurityGroupRules': -1,
                    'maxSecurityGroups': -1,
                    'maxServerGroupMembers': -1,
                    'maxServerGroups': -1,
                    'maxServerMeta': -1,
                    'maxTotalCores': -1,
                    'maxTotalFloatingIps': -1,
                    'maxTotalInstances': -1,
                    'maxTotalKeypairs': -1,
                    'maxTotalRAMSize': -1,
                    'totalCoresUsed': -1,
                    'totalFloatingIpsUsed': -1,
                    'totalInstancesUsed': -1,
                    'totalRAMUsed': -1,
                    'totalSecurityGroupsUsed': -1,
                    'totalServerGroupsUsed': -1,
                },
            },
        }
        self.assertEqual(expected_response, response)

    def test_index_v275(self):
        req = fakes.HTTPRequest.blank("/?tenant_id=faketenant",
                                      version='2.75')
        response = self.controller.index(req)
        expected_response = {
            "limits": {
                "rate": [],
                "absolute": {
                    'maxServerGroupMembers': -1,
                    'maxServerGroups': -1,
                    'maxServerMeta': -1,
                    'maxTotalCores': -1,
                    'maxTotalInstances': -1,
                    'maxTotalKeypairs': -1,
                    'maxTotalRAMSize': -1,
                    'totalCoresUsed': -1,
                    'totalInstancesUsed': -1,
                    'totalRAMUsed': -1,
                    'totalServerGroupsUsed': -1,
                },
            },
        }
        self.assertEqual(expected_response, response)


class UnifiedLimitsControllerTest(NoopLimitsControllerTest):
    quota_driver = "nova.quota.UnifiedLimitsDriver"

    def setUp(self):
        super(UnifiedLimitsControllerTest, self).setUp()
        reglimits = {local_limit.SERVER_METADATA_ITEMS: 128,
                     local_limit.INJECTED_FILES: 5,
                     local_limit.INJECTED_FILES_CONTENT: 10 * 1024,
                     local_limit.INJECTED_FILES_PATH: 255,
                     local_limit.KEY_PAIRS: 100,
                     local_limit.SERVER_GROUPS: 12,
                     local_limit.SERVER_GROUP_MEMBERS: 10}
        self.useFixture(limit_fixture.LimitFixture(reglimits, {}))

    @mock.patch.object(placement_limit, "get_legacy_counts")
    @mock.patch.object(placement_limit, "get_legacy_project_limits")
    @mock.patch.object(objects.InstanceGroupList, "get_counts")
    def test_index_v21(self, mock_count, mock_proj, mock_kcount):
        mock_proj.return_value = {"instances": 1, "cores": 2, "ram": 3}
        mock_kcount.return_value = {"instances": 4, "cores": 5, "ram": 6}
        mock_count.return_value = {'project': {'server_groups': 9}}
        req = fakes.HTTPRequest.blank("/")
        response = self.controller.index(req)
        expected_response = {
            "limits": {
                "rate": [],
                "absolute": {
                    'maxImageMeta': 128,
                    'maxPersonality': 5,
                    'maxPersonalitySize': 10240,
                    'maxSecurityGroupRules': -1,
                    'maxSecurityGroups': -1,
                    'maxServerGroupMembers': 10,
                    'maxServerGroups': 12,
                    'maxServerMeta': 128,
                    'maxTotalCores': 2,
                    'maxTotalFloatingIps': -1,
                    'maxTotalInstances': 1,
                    'maxTotalKeypairs': 100,
                    'maxTotalRAMSize': 3,
                    'totalCoresUsed': 5,
                    'totalFloatingIpsUsed': 0,
                    'totalInstancesUsed': 4,
                    'totalRAMUsed': 6,
                    'totalSecurityGroupsUsed': 0,
                    'totalServerGroupsUsed': 9,
                },
            },
        }
        self.assertEqual(expected_response, response)

    @mock.patch.object(placement_limit, "get_legacy_counts")
    @mock.patch.object(placement_limit, "get_legacy_project_limits")
    @mock.patch.object(objects.InstanceGroupList, "get_counts")
    def test_index_v275(self, mock_count, mock_proj, mock_kcount):
        mock_proj.return_value = {"instances": 1, "cores": 2, "ram": 3}
        mock_kcount.return_value = {"instances": 4, "cores": 5, "ram": 6}
        mock_count.return_value = {'project': {'server_groups': 9}}
        req = fakes.HTTPRequest.blank("/?tenant_id=faketenant",
                                      version='2.75')
        response = self.controller.index(req)
        expected_response = {
            "limits": {
                "rate": [],
                "absolute": {
                    'maxServerGroupMembers': 10,
                    'maxServerGroups': 12,
                    'maxServerMeta': 128,
                    'maxTotalCores': 2,
                    'maxTotalInstances': 1,
                    'maxTotalKeypairs': 100,
                    'maxTotalRAMSize': 3,
                    'totalCoresUsed': 5,
                    'totalInstancesUsed': 4,
                    'totalRAMUsed': 6,
                    'totalServerGroupsUsed': 9,
                },
            },
        }
        self.assertEqual(expected_response, response)
