#       Licensed to the Apache Software Foundation (ASF) under one
#       or more contributor license agreements.  See the NOTICE file
#       distributed with this work for additional information
#       regarding copyright ownership.  The ASF licenses this file
#       to you 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 pymongo
import mock


import allura
from allura.tests import decorators as td
from allura.model.session import (
    IndexerSessionExtension,
    BatchIndexer,
    ArtifactSessionExtension,
    substitute_extensions,
)


def test_extensions_cm():
    session = mock.Mock(_kwargs=dict(extensions=[]))
    extension = mock.Mock()
    with substitute_extensions(session, [extension]) as sess:
        assert session.flush.call_count == 1
        assert session.close.call_count == 1
        assert sess == session
        assert sess._kwargs['extensions'] == [extension]
    assert session.flush.call_count == 2
    assert session.close.call_count == 2
    assert session._kwargs['extensions'] == []


def test_extensions_cm_raises():
    session = mock.Mock(_kwargs=dict(extensions=[]))
    extension = mock.Mock()
    with td.raises(ValueError), substitute_extensions(session, [extension]) as sess:
        session.flush.side_effect = AttributeError
        assert session.flush.call_count == 1
        assert session.close.call_count == 1
        assert sess == session
        assert sess._kwargs['extensions'] == [extension]
        raise ValueError('test')
    assert session.flush.call_count == 1
    assert session.close.call_count == 1
    assert session._kwargs['extensions'] == []


def test_extensions_cm_flush_raises():
    session = mock.Mock(_kwargs=dict(extensions=[]))
    extension = mock.Mock()
    with td.raises(AttributeError), substitute_extensions(session, [extension]) as sess:
        session.flush.side_effect = AttributeError
        assert session.flush.call_count == 1
        assert session.close.call_count == 1
        assert sess == session
        assert sess._kwargs['extensions'] == [extension]
    assert session.flush.call_count == 2
    assert session.close.call_count == 1
    assert session._kwargs['extensions'] == []


class TestSessionExtension:

    def _mock_indexable(self, **kw):
        m = mock.Mock(**kw)
        m.__ming__ = mock.MagicMock()
        m.index_id.return_value = id(m)
        return m


class TestIndexerSessionExtension(TestSessionExtension):

    def setup_method(self, method):
        session = mock.Mock()
        self.ExtensionClass = IndexerSessionExtension
        self.extension = self.ExtensionClass(session)
        self.tasks = {'add': mock.Mock(), 'del': mock.Mock()}
        self.extension.TASKS = mock.Mock()
        self.extension.TASKS.get.return_value = self.tasks

    def test_flush(self):
        added = [self._mock_indexable(_id=i) for i in (1, 2, 3)]
        modified = [self._mock_indexable(_id=i) for i in (4, 5)]
        deleted = [self._mock_indexable(_id=i) for i in (6, 7)]
        self.extension.objects_added = added
        self.extension.objects_modified = modified
        self.extension.objects_deleted = deleted
        self.extension.after_flush()
        self.tasks['add'].post.assert_called_once_with([1, 2, 3, 4, 5])
        self.tasks['del'].post.assert_called_once_with(list(map(id, deleted)))

    def test_flush_skips_update(self):
        modified = [self._mock_indexable(_id=i) for i in range(5)]
        modified[1].should_update_index.return_value = False
        modified[4].should_update_index.return_value = False
        self.extension.objects_modified = modified
        self.extension.after_flush()
        self.tasks['add'].post.assert_called_once_with([0, 2, 3])

    def test_flush_skips_task_if_all_objects_filtered_out(self):
        modified = [self._mock_indexable(_id=i) for i in range(5)]
        for m in modified:
            m.should_update_index.return_value = False
        self.extension.objects_modified = modified
        self.extension.after_flush()
        assert self.tasks['add'].post.call_count == 0


class TestArtifactSessionExtension(TestSessionExtension):

    def setup_method(self, method):
        session = mock.Mock(disable_index=False)
        self.ExtensionClass = ArtifactSessionExtension
        self.extension = self.ExtensionClass(session)

    @mock.patch.object(allura.model.index.Shortlink, 'from_artifact')
    @mock.patch.object(allura.model.index.ArtifactReference, 'from_artifact')
    @mock.patch('allura.model.session.index_tasks')
    def test_flush_skips_update(self, index_tasks, ref_fa, shortlink_fa):
        modified = [self._mock_indexable(_id=i) for i in range(5)]
        modified[1].should_update_index.return_value = False
        modified[4].should_update_index.return_value = False
        ref_fa.side_effect = lambda obj: mock.Mock(_id=obj._id)
        self.extension.objects_modified = modified
        self.extension.after_flush()
        index_tasks.add_artifacts.post.assert_called_once_with([0, 2, 3])

    @mock.patch('allura.model.session.index_tasks')
    def test_flush_skips_task_if_all_objects_filtered_out(self, index_tasks):
        modified = [self._mock_indexable(_id=i) for i in range(5)]
        for m in modified:
            m.should_update_index.return_value = False
        self.extension.objects_modified = modified
        self.extension.after_flush()
        assert index_tasks.add_artifacts.post.call_count == 0


class TestBatchIndexer:

    def setup_method(self, method):
        session = mock.Mock()
        self.extcls = BatchIndexer
        self.ext = self.extcls(session)

    def _mock_indexable(self, **kw):
        m = mock.Mock(**kw)
        m.index_id.return_value = id(m)
        return m

    @mock.patch('allura.model.ArtifactReference.query.find')
    def test_update_index(self, find):
        m = self._mock_indexable
        objs_deleted = [m(_id=i) for i in (1, 2, 3)]
        arefs = [m(_id=i) for i in (4, 5, 6)]
        find.return_value = [m(_id=i) for i in (7, 8, 9)]
        self.ext.update_index(objs_deleted, arefs)
        assert self.ext.to_delete == \
            {o.index_id() for o in objs_deleted}
        assert self.ext.to_add == {4, 5, 6}

        # test deleting something that was previously added
        objs_deleted += [m(_id=4)]
        find.return_value = [m(_id=4)]
        self.ext.update_index(objs_deleted, [])
        assert self.ext.to_delete == \
            {o.index_id() for o in objs_deleted}
        assert self.ext.to_add == {5, 6}

    @mock.patch('allura.model.session.index_tasks')
    def test_flush(self, index_tasks):
        objs_deleted = [self._mock_indexable(_id=i) for i in (1, 2, 3)]
        del_index_ids = {o.index_id() for o in objs_deleted}
        self.extcls.to_delete = del_index_ids
        self.extcls.to_add = {4, 5, 6}
        self.ext.flush()
        index_tasks.del_artifacts.post.assert_called_once_with(
            list(del_index_ids))
        index_tasks.add_artifacts.post.assert_called_once_with([4, 5, 6])
        assert self.ext.to_delete == set()
        assert self.ext.to_add == set()

    @mock.patch('allura.model.session.index_tasks')
    def test_flush_chunks_huge_lists(self, index_tasks):
        self.extcls.to_delete = set(range(100 * 1000 + 1))
        self.extcls.to_add = set(range(1000 * 1000 + 1))
        self.ext.flush()
        assert \
            len(index_tasks.del_artifacts.post.call_args_list[0][0][0]) == 100 * 1000
        assert \
            len(index_tasks.del_artifacts.post.call_args_list[1][0][0]) == 1
        assert \
            len(index_tasks.add_artifacts.post.call_args_list[0][0][0]) == 1000 * 1000
        assert \
            len(index_tasks.add_artifacts.post.call_args_list[1][0][0]) == 1
        assert self.ext.to_delete == set()
        assert self.ext.to_add == set()

    @mock.patch('allura.tasks.index_tasks')
    def test_flush_noop(self, index_tasks):
        self.ext.flush()
        assert 0 == index_tasks.del_artifacts.post.call_count
        assert 0 == index_tasks.add_artifacts.post.call_count
        assert self.ext.to_delete == set()
        assert self.ext.to_add == set()

    @mock.patch('allura.tasks.index_tasks')
    def test__post_too_large(self, index_tasks):
        def on_post(chunk):
            if len(chunk) > 1:
                e = pymongo.errors.InvalidDocument(
                    "BSON document too large (16906035 bytes) - the connected server supports BSON document sizes up to 16777216 bytes.")
                # ming injects a 2nd arg with the document, so we do too
                e.args = e.args + ("doc:  {'task_name': 'allura.tasks.index_tasks.add_artifacts', ........",)
                raise e
        index_tasks.add_artifacts.post.side_effect = on_post
        self.ext._post(index_tasks.add_artifacts, list(range(5)))
        expected = [
            mock.call([0, 1, 2, 3, 4]),
            mock.call([0, 1]),
            mock.call([0]),
            mock.call([1]),
            mock.call([2, 3, 4]),
            mock.call([2]),
            mock.call([3, 4]),
            mock.call([3]),
            mock.call([4])
        ]
        assert \
            expected == index_tasks.add_artifacts.post.call_args_list

    @mock.patch('allura.tasks.index_tasks')
    def test__post_other_error(self, index_tasks):
        def on_post(chunk):
            raise pymongo.errors.InvalidDocument("Cannot encode object...")
        index_tasks.add_artifacts.post.side_effect = on_post
        with td.raises(pymongo.errors.InvalidDocument):
            self.ext._post(index_tasks.add_artifacts, list(range(5)))
