# Copyright (c) 2010-2012 OpenStack Foundation
#
# 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 unittest
from mock import patch
from swift.proxy.controllers.base import headers_to_container_info, \
    headers_to_account_info, headers_to_object_info, get_container_info, \
    get_container_memcache_key, get_account_info, get_account_memcache_key, \
    get_object_env_key, _get_cache_key, get_info, get_object_info, \
    Controller, GetOrHeadHandler
from swift.common.swob import Request, HTTPException, HeaderKeyDict
from swift.common.utils import split_path
from test.unit import fake_http_connect, FakeRing, FakeMemcache
from swift.proxy import server as proxy_server
from swift.common.request_helpers import get_sys_meta_prefix


FakeResponse_status_int = 201


class FakeResponse(object):
    def __init__(self, headers, env, account, container, obj):
        self.headers = headers
        self.status_int = FakeResponse_status_int
        self.environ = env
        if obj:
            env_key = get_object_env_key(account, container, obj)
        else:
            cache_key, env_key = _get_cache_key(account, container)

        if account and container and obj:
            info = headers_to_object_info(headers, FakeResponse_status_int)
        elif account and container:
            info = headers_to_container_info(headers, FakeResponse_status_int)
        else:
            info = headers_to_account_info(headers, FakeResponse_status_int)
        env[env_key] = info


class FakeRequest(object):
    def __init__(self, env, path, swift_source=None):
        self.environ = env
        (version, account, container, obj) = split_path(path, 2, 4, True)
        self.account = account
        self.container = container
        self.obj = obj
        if obj:
            stype = 'object'
            self.headers = {'content-length': 5555,
                            'content-type': 'text/plain'}
        else:
            stype = container and 'container' or 'account'
            self.headers = {'x-%s-object-count' % (stype): 1000,
                            'x-%s-bytes-used' % (stype): 6666}
        if swift_source:
            meta = 'x-%s-meta-fakerequest-swift-source' % stype
            self.headers[meta] = swift_source

    def get_response(self, app):
        return FakeResponse(self.headers, self.environ, self.account,
                            self.container, self.obj)


class FakeCache(object):
    def __init__(self, val):
        self.val = val

    def get(self, *args):
        return self.val


class TestFuncs(unittest.TestCase):
    def setUp(self):
        self.app = proxy_server.Application(None, FakeMemcache(),
                                            account_ring=FakeRing(),
                                            container_ring=FakeRing(),
                                            object_ring=FakeRing)

    def test_GETorHEAD_base(self):
        base = Controller(self.app)
        req = Request.blank('/v1/a/c/o/with/slashes')
        with patch('swift.proxy.controllers.base.'
                   'http_connect', fake_http_connect(200)):
            resp = base.GETorHEAD_base(req, 'object', FakeRing(), 'part',
                                       '/a/c/o/with/slashes')
        self.assertTrue('swift.object/a/c/o/with/slashes' in resp.environ)
        self.assertEqual(
            resp.environ['swift.object/a/c/o/with/slashes']['status'], 200)
        req = Request.blank('/v1/a/c/o')
        with patch('swift.proxy.controllers.base.'
                   'http_connect', fake_http_connect(200)):
            resp = base.GETorHEAD_base(req, 'object', FakeRing(), 'part',
                                       '/a/c/o')
        self.assertTrue('swift.object/a/c/o' in resp.environ)
        self.assertEqual(resp.environ['swift.object/a/c/o']['status'], 200)
        req = Request.blank('/v1/a/c')
        with patch('swift.proxy.controllers.base.'
                   'http_connect', fake_http_connect(200)):
            resp = base.GETorHEAD_base(req, 'container', FakeRing(), 'part',
                                       '/a/c')
        self.assertTrue('swift.container/a/c' in resp.environ)
        self.assertEqual(resp.environ['swift.container/a/c']['status'], 200)

        req = Request.blank('/v1/a')
        with patch('swift.proxy.controllers.base.'
                   'http_connect', fake_http_connect(200)):
            resp = base.GETorHEAD_base(req, 'account', FakeRing(), 'part',
                                       '/a')
        self.assertTrue('swift.account/a' in resp.environ)
        self.assertEqual(resp.environ['swift.account/a']['status'], 200)

    def test_get_info(self):
        global FakeResponse_status_int
        # Do a non cached call to account
        env = {}
        with patch('swift.proxy.controllers.base.'
                   '_prepare_pre_auth_info_request', FakeRequest):
            info_a = get_info(None, env, 'a')
        # Check that you got proper info
        self.assertEquals(info_a['status'], 201)
        self.assertEquals(info_a['bytes'], 6666)
        self.assertEquals(info_a['total_object_count'], 1000)
        # Make sure the env cache is set
        self.assertEquals(env.get('swift.account/a'), info_a)

        # Do an env cached call to account
        info_a = get_info(None, env, 'a')
        # Check that you got proper info
        self.assertEquals(info_a['status'], 201)
        self.assertEquals(info_a['bytes'], 6666)
        self.assertEquals(info_a['total_object_count'], 1000)
        # Make sure the env cache is set
        self.assertEquals(env.get('swift.account/a'), info_a)

        # This time do env cached call to account and non cached to container
        with patch('swift.proxy.controllers.base.'
                   '_prepare_pre_auth_info_request', FakeRequest):
            info_c = get_info(None, env, 'a', 'c')
        # Check that you got proper info
        self.assertEquals(info_a['status'], 201)
        self.assertEquals(info_c['bytes'], 6666)
        self.assertEquals(info_c['object_count'], 1000)
        # Make sure the env cache is set
        self.assertEquals(env.get('swift.account/a'), info_a)
        self.assertEquals(env.get('swift.container/a/c'), info_c)

        # This time do a non cached call to account than non cached to
        # container
        env = {}  # abandon previous call to env
        with patch('swift.proxy.controllers.base.'
                   '_prepare_pre_auth_info_request', FakeRequest):
            info_c = get_info(None, env, 'a', 'c')
        # Check that you got proper info
        self.assertEquals(info_a['status'], 201)
        self.assertEquals(info_c['bytes'], 6666)
        self.assertEquals(info_c['object_count'], 1000)
        # Make sure the env cache is set
        self.assertEquals(env.get('swift.account/a'), info_a)
        self.assertEquals(env.get('swift.container/a/c'), info_c)

        # This time do an env cached call to container while account is not
        # cached
        del(env['swift.account/a'])
        info_c = get_info(None, env, 'a', 'c')
        # Check that you got proper info
        self.assertEquals(info_a['status'], 201)
        self.assertEquals(info_c['bytes'], 6666)
        self.assertEquals(info_c['object_count'], 1000)
        # Make sure the env cache is set and account still not cached
        self.assertEquals(env.get('swift.container/a/c'), info_c)

        # Do a non cached call to account not found with ret_not_found
        env = {}
        with patch('swift.proxy.controllers.base.'
                   '_prepare_pre_auth_info_request', FakeRequest):
            try:
                FakeResponse_status_int = 404
                info_a = get_info(None, env, 'a', ret_not_found=True)
            finally:
                FakeResponse_status_int = 201
        # Check that you got proper info
        self.assertEquals(info_a['status'], 404)
        self.assertEquals(info_a['bytes'], 6666)
        self.assertEquals(info_a['total_object_count'], 1000)
        # Make sure the env cache is set
        self.assertEquals(env.get('swift.account/a'), info_a)

        # Do a cached call to account not found with ret_not_found
        info_a = get_info(None, env, 'a', ret_not_found=True)
        # Check that you got proper info
        self.assertEquals(info_a['status'], 404)
        self.assertEquals(info_a['bytes'], 6666)
        self.assertEquals(info_a['total_object_count'], 1000)
        # Make sure the env cache is set
        self.assertEquals(env.get('swift.account/a'), info_a)

        # Do a non cached call to account not found without ret_not_found
        env = {}
        with patch('swift.proxy.controllers.base.'
                   '_prepare_pre_auth_info_request', FakeRequest):
            try:
                FakeResponse_status_int = 404
                info_a = get_info(None, env, 'a')
            finally:
                FakeResponse_status_int = 201
        # Check that you got proper info
        self.assertEquals(info_a, None)
        self.assertEquals(env['swift.account/a']['status'], 404)

        # Do a cached call to account not found without ret_not_found
        info_a = get_info(None, env, 'a')
        # Check that you got proper info
        self.assertEquals(info_a, None)
        self.assertEquals(env['swift.account/a']['status'], 404)

    def test_get_container_info_swift_source(self):
        req = Request.blank("/v1/a/c", environ={'swift.cache': FakeCache({})})
        with patch('swift.proxy.controllers.base.'
                   '_prepare_pre_auth_info_request', FakeRequest):
            resp = get_container_info(req.environ, 'app', swift_source='MC')
        self.assertEquals(resp['meta']['fakerequest-swift-source'], 'MC')

    def test_get_object_info_swift_source(self):
        req = Request.blank("/v1/a/c/o",
                            environ={'swift.cache': FakeCache({})})
        with patch('swift.proxy.controllers.base.'
                   '_prepare_pre_auth_info_request', FakeRequest):
            resp = get_object_info(req.environ, 'app', swift_source='LU')
        self.assertEquals(resp['meta']['fakerequest-swift-source'], 'LU')

    def test_get_container_info_no_cache(self):
        req = Request.blank("/v1/AUTH_account/cont",
                            environ={'swift.cache': FakeCache({})})
        with patch('swift.proxy.controllers.base.'
                   '_prepare_pre_auth_info_request', FakeRequest):
            resp = get_container_info(req.environ, 'xxx')
        self.assertEquals(resp['bytes'], 6666)
        self.assertEquals(resp['object_count'], 1000)

    def test_get_container_info_cache(self):
        cached = {'status': 404,
                  'bytes': 3333,
                  'object_count': 10,
                  # simplejson sometimes hands back strings, sometimes unicodes
                  'versions': u"\u1F4A9"}
        req = Request.blank("/v1/account/cont",
                            environ={'swift.cache': FakeCache(cached)})
        with patch('swift.proxy.controllers.base.'
                   '_prepare_pre_auth_info_request', FakeRequest):
            resp = get_container_info(req.environ, 'xxx')
        self.assertEquals(resp['bytes'], 3333)
        self.assertEquals(resp['object_count'], 10)
        self.assertEquals(resp['status'], 404)
        self.assertEquals(resp['versions'], "\xe1\xbd\x8a\x39")

    def test_get_container_info_env(self):
        cache_key = get_container_memcache_key("account", "cont")
        env_key = 'swift.%s' % cache_key
        req = Request.blank("/v1/account/cont",
                            environ={env_key: {'bytes': 3867},
                                     'swift.cache': FakeCache({})})
        resp = get_container_info(req.environ, 'xxx')
        self.assertEquals(resp['bytes'], 3867)

    def test_get_account_info_swift_source(self):
        req = Request.blank("/v1/a", environ={'swift.cache': FakeCache({})})
        with patch('swift.proxy.controllers.base.'
                   '_prepare_pre_auth_info_request', FakeRequest):
            resp = get_account_info(req.environ, 'a', swift_source='MC')
        self.assertEquals(resp['meta']['fakerequest-swift-source'], 'MC')

    def test_get_account_info_no_cache(self):
        req = Request.blank("/v1/AUTH_account",
                            environ={'swift.cache': FakeCache({})})
        with patch('swift.proxy.controllers.base.'
                   '_prepare_pre_auth_info_request', FakeRequest):
            resp = get_account_info(req.environ, 'xxx')
        self.assertEquals(resp['bytes'], 6666)
        self.assertEquals(resp['total_object_count'], 1000)

    def test_get_account_info_cache(self):
        # The original test that we prefer to preserve
        cached = {'status': 404,
                  'bytes': 3333,
                  'total_object_count': 10}
        req = Request.blank("/v1/account/cont",
                            environ={'swift.cache': FakeCache(cached)})
        with patch('swift.proxy.controllers.base.'
                   '_prepare_pre_auth_info_request', FakeRequest):
            resp = get_account_info(req.environ, 'xxx')
        self.assertEquals(resp['bytes'], 3333)
        self.assertEquals(resp['total_object_count'], 10)
        self.assertEquals(resp['status'], 404)

        # Here is a more realistic test
        cached = {'status': 404,
                  'bytes': '3333',
                  'container_count': '234',
                  'total_object_count': '10',
                  'meta': {}}
        req = Request.blank("/v1/account/cont",
                            environ={'swift.cache': FakeCache(cached)})
        with patch('swift.proxy.controllers.base.'
                   '_prepare_pre_auth_info_request', FakeRequest):
            resp = get_account_info(req.environ, 'xxx')
        self.assertEquals(resp['status'], 404)
        self.assertEquals(resp['bytes'], '3333')
        self.assertEquals(resp['container_count'], 234)
        self.assertEquals(resp['meta'], {})
        self.assertEquals(resp['total_object_count'], '10')

    def test_get_account_info_env(self):
        cache_key = get_account_memcache_key("account")
        env_key = 'swift.%s' % cache_key
        req = Request.blank("/v1/account",
                            environ={env_key: {'bytes': 3867},
                                     'swift.cache': FakeCache({})})
        resp = get_account_info(req.environ, 'xxx')
        self.assertEquals(resp['bytes'], 3867)

    def test_get_object_info_env(self):
        cached = {'status': 200,
                  'length': 3333,
                  'type': 'application/json',
                  'meta': {}}
        env_key = get_object_env_key("account", "cont", "obj")
        req = Request.blank("/v1/account/cont/obj",
                            environ={env_key: cached,
                                     'swift.cache': FakeCache({})})
        resp = get_object_info(req.environ, 'xxx')
        self.assertEquals(resp['length'], 3333)
        self.assertEquals(resp['type'], 'application/json')

    def test_get_object_info_no_env(self):
        req = Request.blank("/v1/account/cont/obj",
                            environ={'swift.cache': FakeCache({})})
        with patch('swift.proxy.controllers.base.'
                   '_prepare_pre_auth_info_request', FakeRequest):
            resp = get_object_info(req.environ, 'xxx')
        self.assertEquals(resp['length'], 5555)
        self.assertEquals(resp['type'], 'text/plain')

    def test_headers_to_container_info_missing(self):
        resp = headers_to_container_info({}, 404)
        self.assertEquals(resp['status'], 404)
        self.assertEquals(resp['read_acl'], None)
        self.assertEquals(resp['write_acl'], None)

    def test_headers_to_container_info_meta(self):
        headers = {'X-Container-Meta-Whatevs': 14,
                   'x-container-meta-somethingelse': 0}
        resp = headers_to_container_info(headers.items(), 200)
        self.assertEquals(len(resp['meta']), 2)
        self.assertEquals(resp['meta']['whatevs'], 14)
        self.assertEquals(resp['meta']['somethingelse'], 0)

    def test_headers_to_container_info_sys_meta(self):
        prefix = get_sys_meta_prefix('container')
        headers = {'%sWhatevs' % prefix: 14,
                   '%ssomethingelse' % prefix: 0}
        resp = headers_to_container_info(headers.items(), 200)
        self.assertEquals(len(resp['sysmeta']), 2)
        self.assertEquals(resp['sysmeta']['whatevs'], 14)
        self.assertEquals(resp['sysmeta']['somethingelse'], 0)

    def test_headers_to_container_info_values(self):
        headers = {
            'x-container-read': 'readvalue',
            'x-container-write': 'writevalue',
            'x-container-sync-key': 'keyvalue',
            'x-container-meta-access-control-allow-origin': 'here',
        }
        resp = headers_to_container_info(headers.items(), 200)
        self.assertEquals(resp['read_acl'], 'readvalue')
        self.assertEquals(resp['write_acl'], 'writevalue')
        self.assertEquals(resp['cors']['allow_origin'], 'here')

        headers['x-unused-header'] = 'blahblahblah'
        self.assertEquals(
            resp,
            headers_to_container_info(headers.items(), 200))

    def test_headers_to_account_info_missing(self):
        resp = headers_to_account_info({}, 404)
        self.assertEquals(resp['status'], 404)
        self.assertEquals(resp['bytes'], None)
        self.assertEquals(resp['container_count'], None)

    def test_headers_to_account_info_meta(self):
        headers = {'X-Account-Meta-Whatevs': 14,
                   'x-account-meta-somethingelse': 0}
        resp = headers_to_account_info(headers.items(), 200)
        self.assertEquals(len(resp['meta']), 2)
        self.assertEquals(resp['meta']['whatevs'], 14)
        self.assertEquals(resp['meta']['somethingelse'], 0)

    def test_headers_to_account_info_sys_meta(self):
        prefix = get_sys_meta_prefix('account')
        headers = {'%sWhatevs' % prefix: 14,
                   '%ssomethingelse' % prefix: 0}
        resp = headers_to_account_info(headers.items(), 200)
        self.assertEquals(len(resp['sysmeta']), 2)
        self.assertEquals(resp['sysmeta']['whatevs'], 14)
        self.assertEquals(resp['sysmeta']['somethingelse'], 0)

    def test_headers_to_account_info_values(self):
        headers = {
            'x-account-object-count': '10',
            'x-account-container-count': '20',
        }
        resp = headers_to_account_info(headers.items(), 200)
        self.assertEquals(resp['total_object_count'], '10')
        self.assertEquals(resp['container_count'], '20')

        headers['x-unused-header'] = 'blahblahblah'
        self.assertEquals(
            resp,
            headers_to_account_info(headers.items(), 200))

    def test_headers_to_object_info_missing(self):
        resp = headers_to_object_info({}, 404)
        self.assertEquals(resp['status'], 404)
        self.assertEquals(resp['length'], None)
        self.assertEquals(resp['etag'], None)

    def test_headers_to_object_info_meta(self):
        headers = {'X-Object-Meta-Whatevs': 14,
                   'x-object-meta-somethingelse': 0}
        resp = headers_to_object_info(headers.items(), 200)
        self.assertEquals(len(resp['meta']), 2)
        self.assertEquals(resp['meta']['whatevs'], 14)
        self.assertEquals(resp['meta']['somethingelse'], 0)

    def test_headers_to_object_info_values(self):
        headers = {
            'content-length': '1024',
            'content-type': 'application/json',
        }
        resp = headers_to_object_info(headers.items(), 200)
        self.assertEquals(resp['length'], '1024')
        self.assertEquals(resp['type'], 'application/json')

        headers['x-unused-header'] = 'blahblahblah'
        self.assertEquals(
            resp,
            headers_to_object_info(headers.items(), 200))

    def test_have_quorum(self):
        base = Controller(self.app)
        # just throw a bunch of test cases at it
        self.assertEqual(base.have_quorum([201, 404], 3), False)
        self.assertEqual(base.have_quorum([201, 201], 4), False)
        self.assertEqual(base.have_quorum([201, 201, 404, 404], 4), False)
        self.assertEqual(base.have_quorum([201, 503, 503, 201], 4), False)
        self.assertEqual(base.have_quorum([201, 201], 3), True)
        self.assertEqual(base.have_quorum([404, 404], 3), True)
        self.assertEqual(base.have_quorum([201, 201], 2), True)
        self.assertEqual(base.have_quorum([404, 404], 2), True)
        self.assertEqual(base.have_quorum([201, 404, 201, 201], 4), True)

    def test_range_fast_forward(self):
        req = Request.blank('/')
        handler = GetOrHeadHandler(None, req, None, None, None, None, {})
        handler.fast_forward(50)
        self.assertEquals(handler.backend_headers['Range'], 'bytes=50-')

        handler = GetOrHeadHandler(None, req, None, None, None, None,
                                   {'Range': 'bytes=23-50'})
        handler.fast_forward(20)
        self.assertEquals(handler.backend_headers['Range'], 'bytes=43-50')
        self.assertRaises(HTTPException,
                          handler.fast_forward, 80)

        handler = GetOrHeadHandler(None, req, None, None, None, None,
                                   {'Range': 'bytes=23-'})
        handler.fast_forward(20)
        self.assertEquals(handler.backend_headers['Range'], 'bytes=43-')

        handler = GetOrHeadHandler(None, req, None, None, None, None,
                                   {'Range': 'bytes=-100'})
        handler.fast_forward(20)
        self.assertEquals(handler.backend_headers['Range'], 'bytes=-80')

    def test_transfer_headers_with_sysmeta(self):
        base = Controller(self.app)
        good_hdrs = {'x-base-sysmeta-foo': 'ok',
                     'X-Base-sysmeta-Bar': 'also ok'}
        bad_hdrs = {'x-base-sysmeta-': 'too short'}
        hdrs = dict(good_hdrs)
        hdrs.update(bad_hdrs)
        dst_hdrs = HeaderKeyDict()
        base.transfer_headers(hdrs, dst_hdrs)
        self.assertEqual(HeaderKeyDict(good_hdrs), dst_hdrs)

    def test_generate_request_headers(self):
        base = Controller(self.app)
        src_headers = {'x-remove-base-meta-owner': 'x',
                       'x-base-meta-size': '151M',
                       'new-owner': 'Kun'}
        req = Request.blank('/v1/a/c/o', headers=src_headers)
        dst_headers = base.generate_request_headers(req, transfer=True)
        expected_headers = {'x-base-meta-owner': '',
                            'x-base-meta-size': '151M'}
        for k, v in expected_headers.iteritems():
            self.assertTrue(k in dst_headers)
            self.assertEqual(v, dst_headers[k])
        self.assertFalse('new-owner' in dst_headers)

    def test_generate_request_headers_with_sysmeta(self):
        base = Controller(self.app)
        good_hdrs = {'x-base-sysmeta-foo': 'ok',
                     'X-Base-sysmeta-Bar': 'also ok'}
        bad_hdrs = {'x-base-sysmeta-': 'too short'}
        hdrs = dict(good_hdrs)
        hdrs.update(bad_hdrs)
        req = Request.blank('/v1/a/c/o', headers=hdrs)
        dst_headers = base.generate_request_headers(req, transfer=True)
        for k, v in good_hdrs.iteritems():
            self.assertTrue(k.lower() in dst_headers)
            self.assertEqual(v, dst_headers[k.lower()])
        for k, v in bad_hdrs.iteritems():
            self.assertFalse(k.lower() in dst_headers)
