# 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 collections
import itertools
import json
import mock
import six
import unittest

from swift.cli import container_deleter
from swift.common import internal_client
from swift.common import swob
from swift.common import utils

AppCall = collections.namedtuple('AppCall', [
    'method', 'path', 'query', 'headers', 'body'])


class FakeInternalClient(internal_client.InternalClient):
    def __init__(self, responses):
        self.resp_iter = iter(responses)
        self.calls = []

    def make_request(self, method, path, headers, acceptable_statuses,
                     body_file=None, params=None):
        if body_file is None:
            body = None
        else:
            body = body_file.read()
        path, _, query = path.partition('?')
        self.calls.append(AppCall(method, path, query, headers, body))
        resp = next(self.resp_iter)
        if isinstance(resp, Exception):
            raise resp
        return resp

    def __enter__(self):
        return self

    def __exit__(self, *args):
        unused_responses = [r for r in self.resp_iter]
        if unused_responses:
            raise Exception('Unused responses: %r' % unused_responses)


class TestContainerDeleter(unittest.TestCase):
    def setUp(self):
        patcher = mock.patch.object(container_deleter.time, 'time',
                                    side_effect=itertools.count())
        patcher.__enter__()
        self.addCleanup(patcher.__exit__)

        patcher = mock.patch.object(container_deleter, 'OBJECTS_PER_UPDATE', 5)
        patcher.__enter__()
        self.addCleanup(patcher.__exit__)

    def test_make_delete_jobs(self):
        ts = '1558463777.42739'
        self.assertEqual(
            container_deleter.make_delete_jobs(
                'acct', 'cont', ['obj1', 'obj2'],
                utils.Timestamp(ts)),
            [{'name': ts.split('.')[0] + '-acct/cont/obj1',
              'deleted': 0,
              'created_at': ts,
              'etag': utils.MD5_OF_EMPTY_STRING,
              'size': 0,
              'storage_policy_index': 0,
              'content_type': 'application/async-deleted'},
             {'name': ts.split('.')[0] + '-acct/cont/obj2',
              'deleted': 0,
              'created_at': ts,
              'etag': utils.MD5_OF_EMPTY_STRING,
              'size': 0,
              'storage_policy_index': 0,
              'content_type': 'application/async-deleted'}])

    def test_make_delete_jobs_native_utf8(self):
        ts = '1558463777.42739'
        uacct = acct = u'acct-\U0001f334'
        ucont = cont = u'cont-\N{SNOWMAN}'
        uobj1 = obj1 = u'obj-\N{GREEK CAPITAL LETTER ALPHA}'
        uobj2 = obj2 = u'/obj-\N{GREEK CAPITAL LETTER OMEGA}'
        if six.PY2:
            acct = acct.encode('utf8')
            cont = cont.encode('utf8')
            obj1 = obj1.encode('utf8')
            obj2 = obj2.encode('utf8')
        self.assertEqual(
            container_deleter.make_delete_jobs(
                acct, cont, [obj1, obj2], utils.Timestamp(ts)),
            [{'name': u'%s-%s/%s/%s' % (ts.split('.')[0], uacct, ucont, uobj1),
              'deleted': 0,
              'created_at': ts,
              'etag': utils.MD5_OF_EMPTY_STRING,
              'size': 0,
              'storage_policy_index': 0,
              'content_type': 'application/async-deleted'},
             {'name': u'%s-%s/%s/%s' % (ts.split('.')[0], uacct, ucont, uobj2),
              'deleted': 0,
              'created_at': ts,
              'etag': utils.MD5_OF_EMPTY_STRING,
              'size': 0,
              'storage_policy_index': 0,
              'content_type': 'application/async-deleted'}])

    def test_make_delete_jobs_unicode_utf8(self):
        ts = '1558463777.42739'
        acct = u'acct-\U0001f334'
        cont = u'cont-\N{SNOWMAN}'
        obj1 = u'obj-\N{GREEK CAPITAL LETTER ALPHA}'
        obj2 = u'obj-\N{GREEK CAPITAL LETTER OMEGA}'
        self.assertEqual(
            container_deleter.make_delete_jobs(
                acct, cont, [obj1, obj2], utils.Timestamp(ts)),
            [{'name': u'%s-%s/%s/%s' % (ts.split('.')[0], acct, cont, obj1),
              'deleted': 0,
              'created_at': ts,
              'etag': utils.MD5_OF_EMPTY_STRING,
              'size': 0,
              'storage_policy_index': 0,
              'content_type': 'application/async-deleted'},
             {'name': u'%s-%s/%s/%s' % (ts.split('.')[0], acct, cont, obj2),
              'deleted': 0,
              'created_at': ts,
              'etag': utils.MD5_OF_EMPTY_STRING,
              'size': 0,
              'storage_policy_index': 0,
              'content_type': 'application/async-deleted'}])

    def test_mark_for_deletion_empty_no_yield(self):
        with FakeInternalClient([
            swob.Response(json.dumps([
            ])),
        ]) as swift:
            self.assertEqual(container_deleter.mark_for_deletion(
                swift,
                'account',
                'container',
                'marker',
                'end',
                'prefix',
                timestamp=None,
                yield_time=None,
            ), 0)
            self.assertEqual(swift.calls, [
                ('GET', '/v1/account/container',
                 'format=json&marker=marker&end_marker=end&prefix=prefix',
                 {}, None),
            ])

    def test_mark_for_deletion_empty_with_yield(self):
        with FakeInternalClient([
            swob.Response(json.dumps([
            ])),
        ]) as swift:
            self.assertEqual(list(container_deleter.mark_for_deletion(
                swift,
                'account',
                'container',
                'marker',
                'end',
                'prefix',
                timestamp=None,
                yield_time=0.5,
            )), [(0, None)])
            self.assertEqual(swift.calls, [
                ('GET', '/v1/account/container',
                 'format=json&marker=marker&end_marker=end&prefix=prefix',
                 {}, None),
            ])

    def test_mark_for_deletion_one_update_no_yield(self):
        ts = '1558463777.42739'
        with FakeInternalClient([
            swob.Response(json.dumps([
                {'name': '/obj1'},
                {'name': 'obj2'},
                {'name': 'obj3'},
            ])),
            swob.Response(json.dumps([
            ])),
            swob.Response(status=202),
        ]) as swift:
            self.assertEqual(container_deleter.mark_for_deletion(
                swift,
                'account',
                'container',
                '',
                '',
                '',
                timestamp=utils.Timestamp(ts),
                yield_time=None,
            ), 3)
            self.assertEqual(swift.calls, [
                ('GET', '/v1/account/container',
                 'format=json&marker=&end_marker=&prefix=', {}, None),
                ('GET', '/v1/account/container',
                 'format=json&marker=obj3&end_marker=&prefix=', {}, None),
                ('UPDATE', '/v1/.expiring_objects/' + ts.split('.')[0], '', {
                    'X-Backend-Allow-Private-Methods': 'True',
                    'X-Backend-Storage-Policy-Index': '0',
                    'X-Timestamp': ts}, mock.ANY),
            ])
            self.assertEqual(
                json.loads(swift.calls[-1].body),
                container_deleter.make_delete_jobs(
                    'account', 'container', ['/obj1', 'obj2', 'obj3'],
                    utils.Timestamp(ts)
                )
            )

    def test_mark_for_deletion_two_updates_with_yield(self):
        ts = '1558463777.42739'
        with FakeInternalClient([
            swob.Response(json.dumps([
                {'name': 'obj1'},
                {'name': 'obj2'},
                {'name': 'obj3'},
                {'name': u'obj4-\N{SNOWMAN}'},
                {'name': 'obj5'},
                {'name': 'obj6'},
            ])),
            swob.Response(status=202),
            swob.Response(json.dumps([
            ])),
            swob.Response(status=202),
        ]) as swift:
            self.assertEqual(list(container_deleter.mark_for_deletion(
                swift,
                'account',
                'container',
                '',
                'end',
                'pre',
                timestamp=utils.Timestamp(ts),
                yield_time=0,
            )), [(5, 'obj5'), (6, 'obj6'), (6, None)])
            self.assertEqual(swift.calls, [
                ('GET', '/v1/account/container',
                 'format=json&marker=&end_marker=end&prefix=pre', {}, None),
                ('UPDATE', '/v1/.expiring_objects/' + ts.split('.')[0], '', {
                    'X-Backend-Allow-Private-Methods': 'True',
                    'X-Backend-Storage-Policy-Index': '0',
                    'X-Timestamp': ts}, mock.ANY),
                ('GET', '/v1/account/container',
                 'format=json&marker=obj6&end_marker=end&prefix=pre',
                 {}, None),
                ('UPDATE', '/v1/.expiring_objects/' + ts.split('.')[0], '', {
                    'X-Backend-Allow-Private-Methods': 'True',
                    'X-Backend-Storage-Policy-Index': '0',
                    'X-Timestamp': ts}, mock.ANY),
            ])
            self.assertEqual(
                json.loads(swift.calls[-3].body),
                container_deleter.make_delete_jobs(
                    'account', 'container',
                    ['obj1', 'obj2', 'obj3', u'obj4-\N{SNOWMAN}', 'obj5'],
                    utils.Timestamp(ts)
                )
            )
            self.assertEqual(
                json.loads(swift.calls[-1].body),
                container_deleter.make_delete_jobs(
                    'account', 'container', ['obj6'],
                    utils.Timestamp(ts)
                )
            )