# -*- coding: utf-8 -*-
import os
import time
import Image, StringIO
import allura

from mock import patch
from nose.tools import assert_true, assert_false, assert_equal, assert_in
from formencode.variabledecode import variable_encode

from alluratest.controller import TestController
from allura import model as M
from forgewiki import model as wm
from forgetracker import model as tm

from allura.lib.security import has_access
from allura.lib import helpers as h
from allura.tests import decorators as td
from ming.orm.ormsession import ThreadLocalORMSession

class TrackerTestController(TestController):
    def setUp(self):
        super(TrackerTestController, self).setUp()
        self.setup_with_tools()

    @td.with_tracker
    def setup_with_tools(self):
        pass

    def new_ticket(self, mount_point='/bugs/', extra_environ=None, **kw):
        extra_environ = extra_environ or {}
        response = self.app.get(mount_point + 'new/',
                                extra_environ=extra_environ)
        form = response.forms[1]
        for k, v in kw.iteritems():
            form['ticket_form.%s' % k] = v
        resp = form.submit()
        assert resp.status_int != 200, resp
        return resp

class TestMilestones(TrackerTestController):
    def test_milestone_list(self):
        r = self.app.get('/bugs/milestones')
        assert '1.0' in r, r.showbrowser()

    def test_milestone_list_progress(self):
        self.new_ticket(summary='foo', _milestone='1.0')
        self.new_ticket(summary='bar', _milestone='1.0', status='closed')
        r = self.app.get('/bugs/milestones')
        assert '1 / 2' in r, r.showbrowser()

    def test_default_milestone_created_if_missing(self):
        p = M.Project.query.get(shortname='test')
        app = p.app_instance('bugs')
        app.globals.custom_fields = []
        ThreadLocalORMSession.flush_all()
        d = {
            'field_name':'_milestone',
            'milestones-0.old_name':'',
            'milestones-0.new_name':'1.0',
            'milestones-0.description':'Version 1',
            'milestones-0.complete':'Open',
            'milestones-0.due_date':''
        }
        r = self.app.post('/bugs/update_milestones', d)
        r = self.app.get('/bugs/milestones')
        assert 'Version 1' in r
        # make sure _milestone doesn't get created again if it already exists
        r = self.app.post('/bugs/update_milestones', d)
        p = M.Project.query.get(shortname='test')
        app = p.app_instance('bugs')
        assert len(app.globals.custom_fields) == 1, len(app.globals.custom_fields)

def post_install_create_ticket_permission(app):
    """Set to authenticated permission to create tickets but not update"""
    role = M.ProjectRole.by_name('*authenticated')._id
    create_permission = M.ACE.allow(role, 'create')
    update_permission = M.ACE.allow(role, 'update')
    acl = app.config.acl
    acl.append(create_permission)
    if update_permission in acl:
        acl.remove(update_permission)

def post_install_update_ticket_permission(app):
    """Set to anonymous permission to create and update tickets"""
    role = M.ProjectRole.by_name('*anonymous')._id
    app.config.acl.append(M.ACE.allow(role, 'create'))
    app.config.acl.append(M.ACE.allow(role, 'update'))


class TestFunctionalController(TrackerTestController):
    def test_bad_ticket_number(self):
        self.app.get('/bugs/input.project_user_select', status=404)

    def test_invalid_ticket(self):
        self.app.get('/bugs/2/', status=404)

    def test_new_ticket(self):
        summary = 'test new ticket'
        ticket_view = self.new_ticket(summary=summary).follow()
        assert_true(summary in ticket_view)
        assert 'class="artifact_subscribe' in ticket_view

    def test_new_with_milestone(self):
        ticket_view = self.new_ticket(summary='test new with milestone', **{'_milestone':'1.0'}).follow()
        assert 'Milestone' in ticket_view
        assert '1.0' in ticket_view

    def test_milestone_count(self):
        self.new_ticket(summary='test new with milestone', **{'_milestone':'1.0'})
        self.new_ticket(summary='test new with milestone', **{'_milestone':'1.0',
                                                              'private': '1'})
        r = self.app.get('/bugs/')
        assert '<small>2</small>' in r
        # Private tickets shouldn't be included in counts if user doesn't
        # have read access to private tickets.
        r = self.app.get('/bugs/', extra_environ=dict(username='*anonymous'))
        assert '<small>1</small>' in r

    def test_milestone_progress(self):
        self.new_ticket(summary='Ticket 1', **{'_milestone':'1.0'})
        self.new_ticket(summary='Ticket 2', **{'_milestone':'1.0',
                                               'status': 'closed',
                                               'private': '1'}).follow()
        r = self.app.get('/bugs/milestone/1.0/')
        assert '1 / 2' in r
        # Private tickets shouldn't be included in counts if user doesn't
        # have read access to private tickets.
        r = self.app.get('/bugs/milestone/1.0/',
                         extra_environ=dict(username='*anonymous'))
        assert '0 / 1' in r

    def test_new_ticket_form(self):
        response = self.app.get('/bugs/new/')
        form = response.forms[1]
        form['ticket_form.summary'] = 'test new ticket form'
        form['ticket_form.assigned_to'] = 'test_admin'
        response = form.submit().follow()
        assert 'Test Admin' in response

    def test_private_ticket(self):
        ticket_view = self.new_ticket(summary='Public Ticket').follow()
        assert_true('<label class="simple">Private:</label> No' in ticket_view)
        ticket_view = self.new_ticket(summary='Private Ticket',
                                      private=True).follow()
        assert_true('<label class="simple">Private:</label> Yes' in ticket_view)
        M.MonQTask.run_ready()
        # Creator sees private ticket on list page...
        index_response = self.app.get('/p/test/bugs/')
        assert '2 results' in index_response
        assert 'Public Ticket' in index_response
        assert 'Private Ticket' in index_response
        # ...and in search results.
        search_response = self.app.get('/p/test/bugs/search/?q=ticket')
        assert '2 results' in search_response
        assert 'Private Ticket' in search_response
        # Unauthorized user doesn't see private ticket on list page...
        env = dict(username='*anonymous')
        r = self.app.get('/p/test/bugs/', extra_environ=env)
        assert '1 results' in r
        assert 'Private Ticket' not in r
        # ...or in search results...
        r = self.app.get('/p/test/bugs/search/?q=ticket', extra_environ=env)
        assert '1 results' in r
        assert 'Private Ticket' not in r
        # ... or in search feed...
        r = self.app.get('/p/test/bugs/search_feed?q=ticket', extra_environ=env)
        assert 'Private Ticket' not in r
        # ...and can't get to the private ticket directly.
        r = self.app.get(ticket_view.request.url, extra_environ=env)
        assert 'Private Ticket' not in r
        # ... and it doesn't appear in the feed
        r = self.app.get('/p/test/bugs/feed.atom')
        assert 'Private Ticket' not in r
        # ... or in the API ...
        r = self.app.get('/rest/p/test/bugs/2/')
        assert 'Private Ticket' not in r
        assert '/auth/?return_to' in r.headers['Location']
        r = self.app.get('/rest/p/test/bugs/')
        assert 'Private Ticket' not in r

    @td.with_tool('test', 'Tickets', 'doc-bugs')
    def test_two_trackers(self):
        summary = 'test two trackers'
        ticket_view = self.new_ticket('/doc-bugs/', summary=summary, _milestone='1.0').follow()
        ThreadLocalORMSession.flush_all()
        M.MonQTask.run_ready()
        ThreadLocalORMSession.flush_all()
        assert_true(summary in ticket_view)
        index_view = self.app.get('/doc-bugs/')
        assert_true(summary in index_view)
        assert_true(sidebar_contains(index_view, '<span class="has_small">1.0</span><small>1</small>'))
        index_view = self.app.get('/bugs/')
        assert_false(sidebar_contains(index_view, '<span class="has_small">1.0</span><small>1</small>'))
        assert_false(summary in index_view)

    def test_render_ticket(self):
        summary = 'test render ticket'
        ticket_view = self.new_ticket(summary=summary).follow()
        ticket_view.mustcontain(summary, 'Discussion')

    def test_render_index(self):
        admin = M.User.query.get(username='test-admin')
        anon = M.User.query.get(username="*anonymous")
        for app in M.AppConfig.query.find({'options.mount_point': 'bugs'}):
            assert has_access(app, 'create', admin)
            assert not has_access(app, 'create', anon)

        index_view = self.app.get('/bugs/')
        assert 'No open tickets found.' in index_view
        assert 'Create Ticket' in index_view
        # No 'Create Ticket' button for user without 'create' perm
        r = self.app.get('/bugs/', extra_environ=dict(username='*anonymous'))
        assert 'Create Ticket' not in r

    def test_render_markdown_syntax(self):
        r = self.app.get('/bugs/markdown_syntax')
        assert_true('Markdown Syntax' in r)

    def test_ticket_diffs(self):
        self.new_ticket(summary='difftest', description='1\n2\n3\n')
        self.app.post('/bugs/1/update_ticket',{
            'summary':'difftest',
            'description':'1\n3\n4\n',
        })
        r = self.app.get('/bugs/1/')
        assert '<span class="gd">-2</span>' in r, r.showbrowser()
        assert '<span class="gi">+4</span>' in r, r.showbrowser()

    def test_ticket_label_unlabel(self):
        summary = 'test labeling and unlabeling a ticket'
        self.new_ticket(summary=summary)
        self.app.post('/bugs/1/update_ticket',{
            'summary':'aaa',
            'description':'bbb',
            'status':'ccc',
            '_milestone':'',
            'assigned_to':'',
            'labels':u'yellow,greén'.encode('utf-8'),
            'labels_old':u'yellow,greén'.encode('utf-8'),
            'comment': ''
        })
        response = self.app.get('/bugs/1/')
        assert_true('yellow' in response)
        assert_true(u'greén' in response)
        self.app.post('/bugs/1/update_ticket',{
            'summary':'zzz',
            'description':'bbb',
            'status':'ccc',
            '_milestone':'',
            'assigned_to':'',
            'labels':'yellow',
            'labels_old':'yellow',
            'comment': ''
        })
        response = self.app.get('/bugs/1/')
        assert_true('yellow' in response)
        # the following assert is no longer true since "green" is shown in changelog
        # assert_true('green' not in response)

    def test_new_attachment(self):
        file_name = 'test_root.py'
        file_data = file(__file__).read()
        upload = ('attachment', file_name, file_data)
        self.new_ticket(summary='test new attachment')
        ticket_editor = self.app.post('/bugs/1/update_ticket',{
            'summary':'zzz'
        }, upload_files=[upload]).follow()
        assert_true(file_name in ticket_editor)

    def test_delete_attachment(self):
        file_name = 'test_root.py'
        file_data = file(__file__).read()
        upload = ('attachment', file_name, file_data)
        self.new_ticket(summary='test new attachment')
        ticket_editor = self.app.post('/bugs/1/update_ticket',{
            'summary':'zzz'
        }, upload_files=[upload]).follow()
        assert file_name in ticket_editor, ticket_editor.showbrowser()
        req = self.app.get('/bugs/1/')
        file_link = req.html.findAll('form')[1].findAll('a')[7]
        assert_equal(file_link.string, file_name)
        self.app.post(str(file_link['href']),{
            'delete':'True'
        })
        deleted_form = self.app.get('/bugs/1/')
        assert file_name not in deleted_form

    def test_new_text_attachment_content(self):
        file_name = 'test_root.py'
        file_data = file(__file__).read()
        upload = ('attachment', file_name, file_data)
        self.new_ticket(summary='test new attachment')
        ticket_editor = self.app.post('/bugs/1/update_ticket',{
            'summary':'zzz'
        }, upload_files=[upload]).follow()
        download = self.app.get(str(ticket_editor.html.findAll('form')[1].findAll('a')[7]['href']))
        assert_equal(download.body, file_data)

    def test_new_image_attachment_content(self):
        h.set_context('test', 'bugs', neighborhood='Projects')
        file_name = 'neo-icon-set-454545-256x350.png'
        file_path = os.path.join(allura.__path__[0],'nf','allura','images',file_name)
        file_data = file(file_path).read()
        upload = ('attachment', file_name, file_data)
        self.new_ticket(summary='test new attachment')
        ticket_editor = self.app.post('/bugs/1/update_ticket',{
            'summary':'zzz'
        }, upload_files=[upload]).follow()
        ticket = tm.Ticket.query.find({'ticket_num':1}).first()
        filename = ticket.attachments.first().filename

        uploaded = Image.open(file_path)
        r = self.app.get('/bugs/1/attachment/'+filename)
        downloaded = Image.open(StringIO.StringIO(r.body))
        assert uploaded.size == downloaded.size
        r = self.app.get('/bugs/1/attachment/'+filename+'/thumb')

        thumbnail = Image.open(StringIO.StringIO(r.body))
        assert thumbnail.size == (100,100)

    def test_sidebar_static_page(self):
        admin = M.User.query.get(username='test-admin')
        for app in M.AppConfig.query.find({'options.mount_point': 'bugs'}):
            assert has_access(app, 'create', admin)

        response = self.app.get('/bugs/search/')
        assert 'Create Ticket' in response
        assert 'Related Pages' not in response

    def test_related_artifacts(self):
        summary = 'test sidebar logic for a ticket page'
        self.new_ticket(summary=summary)
        response = self.app.get('/p/test/bugs/1/')
        assert 'Related Pages' not in response
        self.app.post('/wiki/aaa/update', params={
                'title':'aaa',
                'text':'',
                'labels':'',
                'labels_old':'',
                'viewable_by-0.id':'all'})
        self.new_ticket(summary='bbb')
        ThreadLocalORMSession.flush_all()
        M.MonQTask.run_ready()
        ThreadLocalORMSession.flush_all()

        h.set_context('test', 'wiki', neighborhood='Projects')
        a = wm.Page.query.find(dict(title='aaa')).first()
        a.text = '\n[bugs:#1]\n'
        ThreadLocalORMSession.flush_all()
        M.MonQTask.run_ready()
        ThreadLocalORMSession.flush_all()
        b = tm.Ticket.query.find(dict(ticket_num=2)).first()
        b.description = '\n[#1]\n'
        ThreadLocalORMSession.flush_all()
        M.MonQTask.run_ready()
        ThreadLocalORMSession.flush_all()

        response = self.app.get('/p/test/bugs/1/')
        assert 'Related' in response
        assert 'Wiki: aaa' in response
        assert 'Ticket: #2' in response

    def test_ticket_view_editable(self):
        summary = 'test ticket view page can be edited'
        self.new_ticket(summary=summary)
        response = self.app.get('/p/test/bugs/1/')
        assert response.html.find('input', {'name': 'ticket_form.summary'})
        assert response.html.find('input', {'name': 'ticket_form.assigned_to'})
        assert response.html.find('textarea', {'name': 'ticket_form.description'})
        assert response.html.find('select', {'name': 'ticket_form.status'})
        assert response.html.find('select', {'name': 'ticket_form._milestone'})
        assert response.html.find('input', {'name': 'ticket_form.labels'})
        assert response.html.find('textarea', {'name': 'ticket_form.comment'})

    def test_assigned_to_nobody(self):
        summary = 'test default assignment'
        self.new_ticket(summary=summary)
        response = self.app.get('/p/test/bugs/1/')
        assert 'nobody' in str(response.html.find('div', {'class': 'grid-5 ticket-assigned-to'}))

    def test_assign_ticket(self):
        summary = 'test assign ticket'
        self.new_ticket(summary=summary)
        response = self.app.get('/p/test/bugs/1/')
        assert 'nobody' in str(response.html.find('div', {'class': 'grid-5 ticket-assigned-to'}))
        response = self.app.post('/bugs/1/update_ticket',{
            'summary':'zzz',
            'description':'bbb',
            'status':'ccc',
            '_milestone':'',
            'assigned_to':'test-admin',
            'labels':'',
            'labels_old':'',
            'comment': ''
        }).follow()
        assert 'test-admin' in str(response.html.find('div', {'class': 'grid-5 ticket-assigned-to'}))
        assert '<li><strong>summary</strong>: test assign ticket --&gt; zzz' in response
        assert '<li><strong>status</strong>: open --&gt; ccc' in response

    def test_custom_fields(self):
        params = dict(
            custom_fields=[
                dict(name='_priority', label='Priority', type='select',
                     options='normal urgent critical'),
                dict(name='_category', label='Category', type='string',
                     options=''),
                dict(name='_code_review', label='Code Review', type='user')],
            open_status_names='aa bb',
            closed_status_names='cc',
            )
        self.app.post(
            '/admin/bugs/set_custom_fields',
            params=variable_encode(params))
        kw = {'custom_fields._priority':'normal',
              'custom_fields._category':'helloworld',
              'custom_fields._code_review':'test-admin'}
        ticket_view = self.new_ticket(summary='test custom fields', **kw).follow()
        assert 'Priority:' in ticket_view
        assert 'normal' in ticket_view
        assert 'Test Admin' in ticket_view

    def test_custom_field_update_comments(self):
        params = dict(
            custom_fields=[
                dict(label='Number', type='number', options='')],
            open_status_names='aa bb',
            closed_status_names='cc',
            )
        r = self.app.post('/admin/bugs/set_custom_fields',
                          params=variable_encode(params))
        kw = {'custom_fields._number':''}
        ticket_view = self.new_ticket(summary='test custom fields', **kw).follow()
        assert '<strong>number</strong>:  --&gt;' not in ticket_view
        ticket_view = self.app.post('/bugs/1/update_ticket',params={
            'summary':'zzz',
            'description':'bbb',
            'status':'ccc',
            '_milestone':'aaa',
            'assigned_to':'',
            'labels':'',
            'labels_old':'',
            'custom_fields._number':'',
            'comment': ''
        }).follow()
        assert '<strong>number</strong>:  --&gt;' not in ticket_view
        ticket_view = self.app.post('/bugs/1/update_ticket',params={
            'summary':'zzz',
            'description':'bbb',
            'status':'ccc',
            '_milestone':'aaa',
            'assigned_to':'',
            'labels':'',
            'labels_old':'',
            'custom_fields._number':'4',
            'comment': ''
        }).follow()
        assert '<strong>number</strong>:  --&gt;' in ticket_view

    def test_milestone_names(self):
        params = {
            'open_status_names': 'aa bb',
            'closed_status_names': 'cc',
            'custom_fields': [dict(
                    label='Milestone',
                    show_in_search='on',
                    type='milestone',
                    milestones=[
                        dict(name='aaaé'),
                        dict(name='bbb'),
                        dict(name='ccc')])] }
        self.app.post('/admin/bugs/set_custom_fields',
                      variable_encode(params),
                      status=302)
        self.new_ticket(summary='test milestone names')
        self.app.post('/bugs/1/update_ticket',{
            'summary':'zzz',
            'description':'bbb',
            'status':'ccc',
            '_milestone':'aaaé',
            'assigned_to':'',
            'labels':'',
            'labels_old':'',
            'comment': ''
        })
        ticket_view = self.app.get('/p/test/bugs/1/')
        assert 'Milestone' in ticket_view
        assert 'aaaé' in ticket_view

    def test_milestone_rename(self):
        self.new_ticket(summary='test milestone rename')
        self.app.post('/bugs/1/update_ticket',{
            'summary':'test milestone rename',
            'description':'',
            'status':'',
            '_milestone':'1.0',
            'assigned_to':'',
            'labels':'',
            'labels_old':'',
            'comment': ''
        })
        ThreadLocalORMSession.flush_all()
        M.MonQTask.run_ready()
        ThreadLocalORMSession.flush_all()
        ticket_view = self.app.get('/p/test/bugs/1/')
        assert 'Milestone' in ticket_view
        assert '1.0' in ticket_view
        assert 'zzzé' not in ticket_view
        r = self.app.post('/bugs/update_milestones',{
            'field_name':'_milestone',
            'milestones-0.old_name':'1.0',
            'milestones-0.new_name':'zzzé',
            'milestones-0.description':'',
            'milestones-0.complete':'Open',
            'milestones-0.due_date':''
        })
        ticket_view = self.app.get('/p/test/bugs/1/')
        assert '1.0' not in ticket_view
        assert 'zzzé' in ticket_view

    def test_milestone_close(self):
        self.new_ticket(summary='test milestone close')
        r = self.app.get('/bugs/milestones')
        assert 'view closed' not in r
        r = self.app.post('/bugs/update_milestones',{
            'field_name':'_milestone',
            'milestones-0.old_name':'1.0',
            'milestones-0.new_name':'1.0',
            'milestones-0.description':'',
            'milestones-0.complete':'Closed',
            'milestones-0.due_date':''
        })
        r = self.app.get('/bugs/milestones')
        assert 'view closed' in r

    def test_subtickets(self):
        # create two tickets
        self.new_ticket(summary='test superticket')
        self.new_ticket(summary='test subticket')
        h.set_context('test', 'bugs', neighborhood='Projects')
        ThreadLocalORMSession.flush_all()
        ThreadLocalORMSession.close_all()
        super = tm.Ticket.query.get(ticket_num=1)
        sub = tm.Ticket.query.get(ticket_num=2)

        # make one ticket a subticket of the other
        sub.set_as_subticket_of(super._id)
        ThreadLocalORMSession.flush_all()

        # get a view on the first ticket, check for other ticket listed in sidebar
        ticket_view = self.app.get('/p/test/bugs/1/')
        assert 'Supertask' not in ticket_view
        assert '[#2]' in ticket_view

        # get a view on the second ticket, check for other ticket listed in sidebar
        ticket_view = self.app.get('/p/test/bugs/2/')
        assert 'Supertask' in ticket_view
        assert '[#1]' in ticket_view

    def test_custom_sums(self):
        # setup a custom sum field
        r = self.app.post('/admin/bugs/set_custom_fields', {
            'custom_fields-0.label': 'days',
            'custom_fields-0.type': 'sum',
            'custom_fields-0.sum': '',
            'custom_fields-0.milestones': '',
            'custom_fields-0.options': '',
            'open_status_names': 'aa bb',
            'closed_status_names': 'cc',
            'milestone_names':'' })
        # create three tickets
        kw = {'custom_fields._days':'0'}
        self.new_ticket(summary='test superticket', **kw)
        self.new_ticket(summary='test subticket-1', **kw)
        self.new_ticket(summary='test subticket-2', **kw)
        ThreadLocalORMSession.flush_all()
        ThreadLocalORMSession.close_all()
        h.set_context('test', 'bugs', neighborhood='Projects')
        super = tm.Ticket.query.get(ticket_num=1)
        sub1 = tm.Ticket.query.get(ticket_num=2)
        sub2 = tm.Ticket.query.get(ticket_num=3)

        # set values for the custom sum
        sub1.custom_fields['_days'] = '4.5'
        sub2.custom_fields['_days'] = '2.0'

        # make two tickets a subtickets of the other
        sub1.set_as_subticket_of(super._id)
        sub2.set_as_subticket_of(super._id)
        ThreadLocalORMSession.flush_all()
        ThreadLocalORMSession.close_all()

        # get a view on the first ticket, check for other ticket listed in sidebar
        ticket_view = self.app.get('/p/test/bugs/1/')
        assert 'days' in ticket_view
        assert '6.5' in ticket_view

    def test_edit_all_button(self):
        admin = M.User.query.get(username='test-admin')
        for app in M.AppConfig.query.find({'options.mount_point': 'bugs'}):
            assert has_access(app, 'update', admin)

        response = self.app.get('/p/test/bugs/search/')
        assert 'Edit All' not in response

    def test_new_ticket_validation(self):
        summary = 'ticket summary'
        response = self.app.get('/bugs/new/')
        assert not response.html.find('div', {'class':'error'})
        form = response.forms[1]
        form['ticket_form.labels'] = 'foo'
        # try submitting with no summary set and check for error message
        error_form = form.submit()
        assert error_form.forms[1]['ticket_form.labels'].value == 'foo'
        error_message = error_form.html.find('div', {'class':'error'})
        assert error_message
        assert (error_message.string == 'You must provide a Title' or \
                error_message.string == 'Missing value')
        assert error_message.findPreviousSibling('input').get('name') == 'ticket_form.summary'
        # set a summary, submit, and check for success
        error_form.forms[1]['ticket_form.summary'] = summary
        success = error_form.forms[1].submit().follow().html
        assert success.findAll('form')[1].get('action') == '/p/test/bugs/1/update_ticket_from_widget'
        assert success.find('input', {'name':'ticket_form.summary'})['value'] == summary

    def test_edit_ticket_validation(self):
        old_summary = 'edit ticket test'
        new_summary = "new summary"
        self.new_ticket(summary=old_summary)
        response = self.app.get('/bugs/1/')
        # check that existing form is valid
        assert response.html.find('input', {'name':'ticket_form.summary'})['value'] == old_summary
        assert not response.html.find('div', {'class':'error'})
        form = response.forms[1]
        # try submitting with no summary set and check for error message
        form['ticket_form.summary'] = ""
        error_form = form.submit()
        error_message = error_form.html.find('div', {'class':'error'})
        assert error_message
        assert error_message.string == 'You must provide a Title'
        assert error_message.findPreviousSibling('input').get('name') == 'ticket_form.summary'
        # set a summary, submit, and check for success
        error_form.forms[1]['ticket_form.summary'] = new_summary
        r = error_form.forms[1].submit()
        assert r.status_int == 302, r.showbrowser()
        success = r.follow().html
        assert success.findAll('form')[1].get('action') == '/p/test/bugs/1/update_ticket_from_widget'
        assert success.find('input', {'name':'ticket_form.summary'})['value'] == new_summary

    def test_home(self):
        self.new_ticket(summary='test first ticket')
        self.new_ticket(summary='test second ticket')
        self.new_ticket(summary='test third ticket')
        ThreadLocalORMSession.flush_all()
        M.MonQTask.run_ready()
        ThreadLocalORMSession.flush_all()
        response = self.app.get('/p/test/bugs/')
        assert 'test third ticket' in response

    def test_search(self):
        self.new_ticket(summary='test first ticket')
        self.new_ticket(summary='test second ticket')
        self.new_ticket(summary='test third ticket')
        ThreadLocalORMSession.flush_all()
        M.MonQTask.run_ready()
        ThreadLocalORMSession.flush_all()
        response = self.app.get('/p/test/bugs/search/?q=test')
        assert '3 results' in response, response.showbrowser()
        assert 'test third ticket' in response, response.showbrowser()

    def test_search_feed(self):
        self.new_ticket(summary='test first ticket')
        ThreadLocalORMSession.flush_all()
        M.MonQTask.run_ready()
        ThreadLocalORMSession.flush_all()
        response = self.app.get('/p/test/bugs/search_feed?q=test')
        assert '<title>test first ticket</title>' in response
        response = self.app.get('/p/test/bugs/search_feed.atom?q=test')
        assert '<title>test first ticket</title>' in response

    def test_touch(self):
        self.new_ticket(summary='test touch')
        h.set_context('test', 'bugs', neighborhood='Projects')
        ticket = tm.Ticket.query.get(ticket_num=1)
        old_date = ticket.mod_date
        ticket.summary = 'changing the summary'
        time.sleep(1)
        ThreadLocalORMSession.flush_all()
        ThreadLocalORMSession.close_all()
        ticket = tm.Ticket.query.get(ticket_num=1)
        new_date = ticket.mod_date
        assert new_date > old_date

    @patch('forgetracker.tracker_main.search_artifact')
    def test_save_invalid_search(self, search_artifact):
        err = 'Error running search query: [Reason: undefined field label]'
        search_artifact.side_effect = ValueError(err)
        r = self.app.post('/admin/bugs/bins/save_bin',{
            'summary': 'This is not too long.',
            'terms': 'label:foo',
            'old_summary': '',
            'sort': ''})
        assert err in r
        r = self.app.get('/admin/bugs/bins/')
        edit_form = r.form
        edit_form['bins-2.summary'] = 'Original'
        edit_form['bins-2.terms'] = 'label:foo'
        r = edit_form.submit()
        assert err in r

    def test_saved_search_labels_truncated(self):
        r = self.app.post('/admin/bugs/bins/save_bin',{
            'summary': 'This is not too long.',
            'terms': 'aaa',
            'old_summary': '',
            'sort': ''}).follow()
        r = self.app.get('/bugs/')
        assert sidebar_contains(r, 'This is not too long.')
        r = self.app.post('/admin/bugs/bins/save_bin',{
            'summary': 'This will be truncated because it is too long to show in the sidebar without being ridiculous.',
            'terms': 'aaa',
            'old_summary': '',
            'sort': ''}).follow()
        r = self.app.get('/bugs/')
        assert sidebar_contains(r, 'This will be truncated because it is too long to show in the sidebar ...')

    def test_edit_saved_search(self):
        r = self.app.get('/admin/bugs/bins/')
        edit_form = r.form
        edit_form['bins-2.summary'] = 'Original'
        edit_form['bins-2.terms'] = 'aaa'
        edit_form.submit()
        r = self.app.get('/bugs/')
        assert sidebar_contains(r, 'Original')
        assert not sidebar_contains(r, 'New')
        r = self.app.get('/admin/bugs/bins/')
        edit_form = r.form
        edit_form['bins-2.summary'] = 'New'
        edit_form.submit()
        r = self.app.get('/bugs/')
        assert sidebar_contains(r, 'New')
        assert not sidebar_contains(r, 'Original')

    def test_discussion_paging(self):
        summary = 'test discussion paging'
        ticket_view = self.new_ticket(summary=summary).follow()
        for f in ticket_view.html.findAll('form'):
            if f.get('action', '').endswith('/post'):
                break
        post_content = 'ticket discussion post content'
        params = dict()
        inputs = f.findAll('input')
        for field in inputs:
            if field.has_key('name'):
                params[field['name']] = field.has_key('value') and field['value'] or ''
        params[f.find('textarea')['name']] = post_content
        r = self.app.post(f['action'].encode('utf-8'), params=params,
                          headers={'Referer': '/bugs/1/'.encode("utf-8")})
        r = self.app.get('/bugs/1/', dict(page=-1))
        assert_true(summary in r)
        r = self.app.get('/bugs/1/', dict(page=1))
        assert_true(post_content in r)
        # no pager if just one page
        assert_false('Page 1 of 1' in r)
        # add some more posts and check for pager
        for i in range(2):
            r = self.app.post(f['action'].encode('utf-8'), params=params,
                  headers={'Referer': '/bugs/1/'.encode("utf-8")})
        r = self.app.get('/bugs/1/', dict(page=1, limit=2))
        assert_true('Page 2 of 2' in r)

    def test_bulk_edit_index(self):
        self.new_ticket(summary='test first ticket', status='open')
        self.new_ticket(summary='test second ticket', status='accepted')
        self.new_ticket(summary='test third ticket', status='closed')
        ThreadLocalORMSession.flush_all()
        M.MonQTask.run_ready()
        ThreadLocalORMSession.flush_all()
        response = self.app.get('/p/test/bugs/?sort=summary+asc')
        ticket_rows = response.html.find('table', {'class':'ticket-list'}).find('tbody')
        assert_in('test first ticket', str(ticket_rows))
        assert_in('test second ticket', str(ticket_rows))
        edit_link = response.html.find('a',{'title':'Bulk Edit'})
        expected_link = "/p/test/bugs/edit/?q=%21status%3Awont-fix+%26%26+%21status%3Aclosed&sort=snippet_s+asc&limit=25&page=0"
        assert_equal(expected_link, edit_link['href'])
        response = self.app.get(edit_link['href'])
        ticket_rows = response.html.find('tbody', {'class':'ticket-list'})
        assert_in('test first ticket', str(ticket_rows))
        assert_in('test second ticket', str(ticket_rows))

    def test_bulk_edit_milestone(self):
        self.new_ticket(summary='test first ticket', status='open', _milestone='1.0')
        self.new_ticket(summary='test second ticket', status='accepted', _milestone='1.0')
        self.new_ticket(summary='test third ticket', status='closed', _milestone='1.0')
        ThreadLocalORMSession.flush_all()
        M.MonQTask.run_ready()
        ThreadLocalORMSession.flush_all()
        response = self.app.get('/p/test/bugs/milestone/1.0/?sort=ticket_num+asc')
        ticket_rows = response.html.find('table', {'class':'ticket-list'}).find('tbody')
        assert_in('test first ticket', str(ticket_rows))
        assert_in('test second ticket', str(ticket_rows))
        assert_in('test third ticket', str(ticket_rows))
        edit_link = response.html.find('a',{'title':'Bulk Edit'})
        expected_link = "/p/test/bugs/edit/?q=_milestone%3A1.0&sort=ticket_num_i+asc&limit=25&page=0"
        assert_equal(expected_link, edit_link['href'])
        response = self.app.get(edit_link['href'])
        ticket_rows = response.html.find('tbody', {'class':'ticket-list'})
        assert_in('test first ticket', str(ticket_rows))
        assert_in('test second ticket', str(ticket_rows))
        assert_in('test third ticket', str(ticket_rows))

    def test_bulk_edit_search(self):
        self.new_ticket(summary='test first ticket', status='open')
        self.new_ticket(summary='test second ticket', status='open')
        self.new_ticket(summary='test third ticket', status='closed', _milestone='1.0')
        ThreadLocalORMSession.flush_all()
        M.MonQTask.run_ready()
        ThreadLocalORMSession.flush_all()
        response = self.app.get('/p/test/bugs/search/?q=status%3Aopen')
        ticket_rows = response.html.find('table', {'class':'ticket-list'}).find('tbody')
        assert_in('test first ticket', str(ticket_rows))
        assert_in('test second ticket', str(ticket_rows))
        assert_false('test third ticket' in str(ticket_rows))
        edit_link = response.html.find('a',{'title':'Bulk Edit'})
        expected_link = "/p/test/bugs/edit/?q=status%3Aopen&limit=25&page=0"
        assert_equal(expected_link, edit_link['href'])
        response = self.app.get(edit_link['href'])
        ticket_rows = response.html.find('tbody', {'class':'ticket-list'})
        assert_in('test first ticket', str(ticket_rows))
        assert_in('test second ticket', str(ticket_rows))
        assert_false('test third ticket' in str(ticket_rows))

    @td.with_tool('test', 'Tickets', 'tracker',
            post_install_hook=post_install_create_ticket_permission)
    def test_create_permission(self):
        """Test that user with `create` permission can create ticket,
        but can't edit it without `update` permission.
        """
        response = self.app.get('/p/test/tracker/',
                                extra_environ=dict(username='test-user-0'))
        assert 'Create Ticket' in response

        response = self.new_ticket(summary='test create, not update',
                                   mount_point='/tracker/',
                                   extra_environ=dict(username='test-user-0'))
        ticket_url = response.headers['Location']
        response = self.app.get(ticket_url,
                                extra_environ=dict(username='test-user-0'))
        assert not response.html.find('div',{'class': 'error'})
        assert not response.html.find('a', {'class': 'edit_ticket'})

    @td.with_tool('test', 'Tickets', 'tracker',
            post_install_hook=post_install_update_ticket_permission)
    def test_update_permission(self):
        r = self.app.get('/p/test/tracker/',
                         extra_environ=dict(username='*anonymous'))
        assert 'Create Ticket' in r

        r = self.new_ticket(summary='test', mount_point='/tracker/',
                            extra_environ=dict(username='*anonymous'))
        ticket_url = r.headers['Location']
        r = self.app.get(ticket_url, extra_environ=dict(username='*anonymous'))
        a = r.html.find('a', {'class': 'edit_ticket'})
        assert a.text == 'Edit'


class TestMilestoneAdmin(TrackerTestController):
    def _post(self, params, **kw):
        params['open_status_names'] = 'aa bb'
        params['closed_status_names'] = 'cc'
        self.app.post('/admin/bugs/set_custom_fields',
                      params=variable_encode(params), **kw)
        return self.app.get('/admin/bugs/fields')

    def _post_milestones(self, milestones):
        params = {'custom_fields': [
            dict(label=mf['label'],
                 show_in_search='on',
                 type='milestone',
                 milestones=[
                    dict((k, v) for k, v in d.iteritems()) for d in mf['milestones']])
            for mf in milestones]}
        return self._post(params)

    def test_create_milestone_field(self):
        r = self._post_milestones([
            dict(label='releases', milestones=[dict(name='1.0/beta')])
        ])
        assert 'releases' in r
        assert '1.0-beta' in r

    def test_delete_milestone_field(self):
        r = self._post_milestones([
            dict(label='releases', milestones=[dict(name='1.0/beta')])
        ])
        self.new_ticket(summary='test new milestone',
                        **{'custom_fields._releases':'1.0-beta'})
        assert tm.Ticket.query.find({
            'custom_fields._releases': '1.0-beta'}).count() == 1
        r = self._post_milestones([])
        assert 'Releases' not in r
        assert '1.0-beta' not in r
        assert tm.Ticket.query.find({
            'custom_fields._releases': '1.0-beta'}).count() == 0

    def test_rename_milestone_field(self):
        r = self._post_milestones([
            dict(label='releases', milestones=[dict(name='1.0/beta')])
        ])
        self.new_ticket(summary='test new milestone',
                        **{'custom_fields._releases':'1.0-beta'})
        r = self._post_milestones([
            dict(label='versions', milestones=[dict(name='1.0/beta')])
        ])
        assert 'Releases' not in r
        assert 'versions' in r
        assert '1.0-beta' in r
        # TODO: This doesn't work - need to make milestone custom fields
        #       renameable.
        #assert tm.Ticket.query.find({
        #    'custom_fields._versions': '1.0-beta'}).count() == 1

    def test_create_milestone(self):
        r = self._post_milestones([
            dict(label='releases', milestones=[dict(name='1.0/beta')])
        ])
        r = self._post_milestones([
            dict(label='releases', milestones=[dict(name='1.0/beta'),
                                               dict(name='2.0')])
        ])
        assert '1.0-beta' in r
        assert '2.0' in r

    def test_delete_milestone(self):
        r = self._post_milestones([
            dict(label='releases', milestones=[dict(name='1.0/beta')])
        ])
        self.new_ticket(summary='test new milestone',
                        **{'custom_fields._releases':'1.0-beta'})
        assert tm.Ticket.query.find({
            'custom_fields._releases': '1.0-beta'}).count() == 1
        r = self._post_milestones([
            dict(label='releases', milestones=[])
        ])
        assert 'releases' in r
        assert '1.0-beta' not in r
        assert tm.Ticket.query.find({
            'custom_fields._releases': '1.0-beta'}).count() == 0

    def test_rename_milestone(self):
        r = self._post_milestones([
            dict(label='releases', milestones=[dict(name='1.0')])
        ])
        self.new_ticket(summary='test new milestone',
                        **{'custom_fields._releases':'1.0'})
        r = self._post_milestones([
            dict(label='releases', milestones=[
                dict(name='1.1', old_name='1.0')])
        ])
        assert 'releases'in r
        assert '1.0' not in r
        assert '1.1' in r
        assert tm.Ticket.query.find({
            'custom_fields._releases': '1.0'}).count() == 0
        assert tm.Ticket.query.find({
            'custom_fields._releases': '1.1'}).count() == 1

def post_install_hook(app):
    role_anon = M.ProjectRole.by_name('*anonymous')._id
    app.config.acl.append(M.ACE.allow(role_anon, 'post'))
    app.config.acl.append(M.ACE.allow(role_anon, 'create'))
    app.config.acl.append(M.ACE.allow(role_anon, 'update'))

class TestEmailMonitoring(TrackerTestController):
    def __init__(self):
        super(TestEmailMonitoring, self).__init__()
        self.test_email = 'mailinglist@example.com'

    def _set_options(self, monitoring_type='AllTicketChanges'):
        r = self.app.post('/admin/bugs/set_options', params={
            'TicketMonitoringEmail': self.test_email,
            'TicketMonitoringType': monitoring_type,
            })
        return r

    def test_set_options(self):
        r = self._set_options()
        r = self.app.get('/admin/bugs/options')
        email = r.html.findAll(attrs=dict(name='TicketMonitoringEmail'))
        mtype = r.html.findAll('option', attrs=dict(value='AllTicketChanges'))
        assert email[0]['value'] == self.test_email
        assert mtype[0]['selected'] == 'selected'

    @td.with_tool('test', 'Tickets', 'doc-bugs', post_install_hook=post_install_hook)
    @patch('forgetracker.model.ticket.Notification.send_direct')
    def test_notifications_moderators(self, send_direct):
        self.new_ticket(summary='test moderation', mount_point='/doc-bugs/')
        self.app.post('/doc-bugs/1/update_ticket',{
            'summary':'test moderation',
            'comment':'test unmoderated post'
        }, extra_environ=dict(username='*anonymous'))
        send_direct.assert_called_with(str(M.User.query.get(username='test-admin')._id))

    @patch('forgetracker.model.ticket.Notification.send_simple')
    def test_notifications_new(self, send_simple):
        self._set_options('NewTicketsOnly')
        self.new_ticket(summary='test')
        self.app.post('/bugs/1/update_ticket',{
            'summary':'test',
            'description':'update',
        })
        send_simple.assert_called_once_with(self.test_email)

    @patch('forgetracker.tracker_main.M.Notification.send_simple')
    def test_notifications_all(self, send_simple):
        self._set_options()
        self.new_ticket(summary='test')
        send_simple.assert_called_once_with(self.test_email)
        send_simple.reset_mock()
        self.app.post('/bugs/1/update_ticket',{
            'summary':'test',
            'description':'update',
        })
        assert send_simple.call_count == 1, send_simple.call_count
        send_simple.assert_called_with(self.test_email)

    @patch('forgetracker.tracker_main.M.Notification.send_simple')
    def test_notifications_off(self, send_simple):
        """Test that tracker notification email is not sent if notifications
        are disabled at the project level.
        """
        p = M.Project.query.get(shortname='test')
        p.notifications_disabled = True
        ThreadLocalORMSession.flush_all()
        self._set_options()
        self.new_ticket(summary='test')
        assert send_simple.call_count == 0, send_simple.call_count

class TestCustomUserField(TrackerTestController):
    def setUp(self):
        super(TestCustomUserField, self).setUp()
        params = dict(
            custom_fields=[
                dict(name='_code_review', label='Code Review', type='user',
                     show_in_search='on')],
            open_status_names='aa bb',
            closed_status_names='cc',
            )
        self.app.post(
            '/admin/bugs/set_custom_fields',
            params=variable_encode(params))

    def test_blank_user(self):
        kw = {'custom_fields._code_review': ''}
        ticket_view = self.new_ticket(summary='test custom fields', **kw).follow()
        # summary header shows 'nobody'
        assert ticket_view.html.findAll('label', 'simple',
            text='Code Review:')[1].parent.parent.text == 'Code Review:nobody'
        # form input is blank
        assert ticket_view.html.find('input',
            dict(name='ticket_form.custom_fields._code_review'))['value'] == ''

    def test_non_project_member(self):
        """ Test that you can't put a non-project-member user in a custom
        user field.
        """
        kw = {'custom_fields._code_review': 'test-user-0'}
        ticket_view = self.new_ticket(summary='test custom fields', **kw).follow()
        # summary header shows 'nobody'
        assert ticket_view.html.findAll('label', 'simple',
            text='Code Review:')[1].parent.parent.text == 'Code Review:nobody'
        # form input is blank
        assert ticket_view.html.find('input',
            dict(name='ticket_form.custom_fields._code_review'))['value'] == ''

    def test_project_member(self):
        kw = {'custom_fields._code_review': 'test-admin'}
        ticket_view = self.new_ticket(summary='test custom fields', **kw).follow()
        # summary header shows 'nobody'
        assert ticket_view.html.findAll('label', 'simple',
            text='Code Review:')[1].parent.parent.text == 'Code Review:Test Admin'
        # form input is blank
        assert ticket_view.html.find('input',
            dict(name='ticket_form.custom_fields._code_review'))['value'] == 'test-admin'

    def test_change_user_field(self):
        kw = {'custom_fields._code_review': ''}
        r = self.new_ticket(summary='test custom fields', **kw).follow()
        f = r.forms[1]
        f['ticket_form.custom_fields._code_review'] = 'test-admin'
        r = f.submit().follow()
        assert '<li><strong>code_review</strong>: Test Admin' in r

    def test_search_results(self):
        kw = {'custom_fields._code_review': 'test-admin'}
        self.new_ticket(summary='test custom fields', **kw)
        r = self.app.get('/bugs/')
        assert r.html.find('table', 'ticket-list').findAll('th')[5].text == 'Code Review'
        assert r.html.find('table', 'ticket-list').tbody.tr.findAll('td')[5].text == 'Test Admin'

class TestHelpTextOptions(TrackerTestController):
    def _set_options(self, new_txt='', search_txt=''):
        r = self.app.post('/admin/bugs/set_options', params={
            'TicketHelpNew': new_txt,
            'TicketHelpSearch': search_txt,
            })
        return r

    def test_help_text(self):
        self._set_options(
                new_txt='**foo**',
                search_txt='*bar*')
        r = self.app.get('/bugs/')
        assert '<em>bar</em>' in r
        r = self.app.get('/bugs/search', params=dict(q='test'))
        assert '<em>bar</em>' in r
        r = self.app.get('/bugs/milestone/1.0/')
        assert '<em>bar</em>' in r
        r = self.app.get('/bugs/new/')
        assert '<strong>foo</strong>' in r

        self._set_options()
        r = self.app.get('/bugs/')
        assert len(r.html.findAll(attrs=dict(id='search-ticket-help-msg'))) == 0
        r = self.app.get('/bugs/search', params=dict(q='test'))
        assert len(r.html.findAll(attrs=dict(id='search-ticket-help-msg'))) == 0
        r = self.app.get('/bugs/milestone/1.0/')
        assert len(r.html.findAll(attrs=dict(id='search-ticket-help-msg'))) == 0
        r = self.app.get('/bugs/new/')
        assert len(r.html.findAll(attrs=dict(id='new-ticket-help-msg'))) == 0

def sidebar_contains(response, text):
    sidebar_menu = response.html.find('div', attrs={'id': 'sidebar'})
    return text in str(sidebar_menu)
