#       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.

from datetime import datetime, timedelta

import mock
from tg import tmpl_context as c
from ming.odm.odmsession import ThreadLocalODMSession

import forgetracker
from forgetracker.model import Globals
from forgetracker.tests.unit import TrackerTestWithModel
from allura.lib import helpers as h


class TestGlobalsModel(TrackerTestWithModel):

    def setup_method(self, method):
        super().setup_method(method)
        c.project.install_app('Tickets', 'doc-bugs')
        ThreadLocalODMSession.flush_all()

    def test_it_has_current_tracker_globals(self):
        bugs_globals = Globals.query.get(app_config_id=c.app.config._id)
        assert c.app.globals == bugs_globals
        h.set_context('test', 'doc-bugs', neighborhood='Projects')
        assert c.app.globals != bugs_globals

    def test_next_ticket_number_increments(self):
        gl = Globals()
        assert gl.next_ticket_num() == 1
        assert gl.next_ticket_num() == 2

    def test_ticket_numbers_are_independent(self):
        with h.push_context('test', 'doc-bugs', neighborhood='Projects'):
            assert c.app.globals.next_ticket_num() == 1
        with h.push_context('test', 'bugs', neighborhood='Projects'):
            assert c.app.globals.next_ticket_num() == 1

    @mock.patch('forgetracker.model.ticket.datetime')
    def test_bin_count(self, mock_dt):
        now = datetime.utcnow()
        mock_dt.utcnow.return_value = now
        gbl = Globals()
        gbl._bin_counts_data = [{'summary': 'foo', 'hits': 1},
                                {'summary': 'bar', 'hits': 2}]
        gbl.invalidate_bin_counts = mock.Mock()

        # not expired, finds bin
        gbl.invalidate_bin_counts.reset_mock()
        gbl._bin_counts_expire = now + timedelta(minutes=5)
        bin = gbl.bin_count('bar')
        assert bin['hits'] == 2
        assert not gbl.invalidate_bin_counts.called

        # expired, returns value for missing bin
        gbl.invalidate_bin_counts.reset_mock()
        gbl._bin_counts_expire = now - timedelta(minutes=5)
        bin = gbl.bin_count('qux')
        assert bin['hits'] == 0
        assert gbl.invalidate_bin_counts.called

        # config set to no expiration
        gbl.invalidate_bin_counts.reset_mock()
        with mock.patch.dict('forgetracker.model.ticket.tg_config', **{'forgetracker.bin_cache_expire': 0}):
            gbl._bin_counts_expire = now - timedelta(minutes=5)
            bin = gbl.bin_count('qux')
            assert not gbl.invalidate_bin_counts.called

        # no expire value (e.g. from previously having config set to no expirations)
        gbl.invalidate_bin_counts.reset_mock()
        gbl._bin_counts_expire = None
        bin = gbl.bin_count('qux')
        assert bin['hits'] == 0
        assert gbl.invalidate_bin_counts.called

    @mock.patch('forgetracker.tasks.update_bin_counts')
    @mock.patch('forgetracker.model.ticket.datetime')
    def test_invalidate_bin_counts(self, mock_dt, mock_task):
        now = datetime.utcnow().replace(microsecond=0)
        mock_dt.utcnow.return_value = now
        gbl = Globals()

        # invalidated recently, don't dog-pile
        gbl._bin_counts_invalidated = now - timedelta(minutes=1)
        gbl.invalidate_bin_counts()
        assert not mock_task.post.called

        # invalidated too long ago, call again
        gbl._bin_counts_invalidated = now - timedelta(minutes=6)
        gbl.invalidate_bin_counts()
        assert mock_task.post.called
        assert gbl._bin_counts_invalidated == now

        # never invalidated
        mock_task.reset_mock()
        gbl._bin_counts_invalidated = None
        gbl.invalidate_bin_counts()
        assert mock_task.post.called
        assert gbl._bin_counts_invalidated == now

    @mock.patch('forgetracker.model.ticket.Bin')
    @mock.patch('forgetracker.model.ticket.search_artifact')
    @mock.patch('forgetracker.model.ticket.datetime')
    def test_update_bin_counts(self, mock_dt, mock_search, mock_bin):
        now = datetime.utcnow().replace(microsecond=0)
        mock_dt.utcnow.return_value = now
        gbl = Globals()
        gbl._bin_counts_invalidated = now - timedelta(minutes=1)
        mock_bin.query.find.return_value = [
            mock.Mock(summary='foo', terms='bar')]
        mock_search().hits = 5

        assert gbl._bin_counts_data == []  # sanity pre-check
        gbl.update_bin_counts()
        assert mock_bin.query.find.called
        mock_search.assert_called_with(
            forgetracker.model.Ticket, 'bar', rows=0, short_timeout=False, fq=['-deleted_b:true'])
        assert gbl._bin_counts_data == [{'summary': 'foo', 'hits': 5}]
        assert gbl._bin_counts_expire == now + timedelta(minutes=60)
        assert gbl._bin_counts_invalidated is None

    def test_append_new_labels(self):
        gbl = Globals()
        assert gbl.append_new_labels([], ['tag1']) == ['tag1']
        assert (
            gbl.append_new_labels(['tag1', 'tag2'], ['tag2']) == ['tag1', 'tag2'])
        assert gbl.append_new_labels(
            ['tag1', 'tag2'], ['tag3']) == ['tag1', 'tag2', 'tag3']
        assert gbl.append_new_labels(
            ['tag1', 'tag2', 'tag3'], ['tag2']) == ['tag1', 'tag2', 'tag3']


class TestCustomFields(TrackerTestWithModel):

    def test_it_has_sortable_custom_fields(self):
        tracker_globals = globals_with_custom_fields(
            [dict(label='Iteration Number',
                  name='_iteration_number',
                  type='number',
                  show_in_search=False),
             dict(label='Point Estimate',
                  name='_point_estimate',
                  type='number',
                  show_in_search=True)])
        expected = [dict(sortable_name='_point_estimate_d',
                         name='_point_estimate',
                         label='Point Estimate')]
        assert tracker_globals.sortable_custom_fields_shown_in_search() == expected


def globals_with_custom_fields(custom_fields):
    c.app.globals.custom_fields = custom_fields
    ThreadLocalODMSession.flush_all()
    return c.app.globals
