# -*- coding: utf-8 -*-
#
# Copyright (C) 2005-2023 Edgewall Software
# All rights reserved.
#
# This software is licensed as described in the file COPYING, which
# you should have received as part of this distribution. The terms
# are also available at https://trac.edgewall.org/wiki/TracLicense.
#
# This software consists of voluntary contributions made by many
# individuals. For the exact contribution history, see the revision
# history and logs, available at https://trac.edgewall.org/log/.

import datetime
import os
import tempfile
import textwrap
import unittest

import jinja2
try:
    from babel.support import LazyProxy
except ImportError:
    LazyProxy = None

from trac.config import ConfigurationError
from trac.core import Component, TracError, implements
from trac.perm import IPermissionRequestor, PermissionSystem
from trac.test import EnvironmentStub, MockPerm, MockRequest, locale_en, \
                      makeSuite, mkdtemp
from trac.tests.contentgen import random_sentence
from trac.resource import Resource
from trac.util import create_file
from trac.util.datefmt import pytz, timezone, utc
from trac.util.html import Markup, tag
from trac.util.translation import has_babel
from trac.web.api import IRequestHandler
from trac.web.chrome import (
    Chrome, INavigationContributor, add_link, add_meta, add_notice,
    add_script, add_script_data, add_stylesheet, add_warning, web_context)
from trac.web.href import Href


class Request(object):
    locale = None
    perm = MockPerm()
    args = {}
    def __init__(self, **kwargs):
        self.chrome = {}
        for k, v in kwargs.items():
            setattr(self, k, v)


class ChromeTestCase(unittest.TestCase):

    def setUp(self):
        self.env = EnvironmentStub(enable=('trac.web.chrome.*',))

    def test_add_meta(self):
        req = MockRequest(self.env)
        add_meta(req, 'Jim Smith', name='Author', scheme='test', lang='en-us')
        add_meta(req, 'Tue, 20 Aug 1996 14:25:27 GMT', http_equiv='Expires')
        metas = req.chrome['metas']
        self.assertEqual(2, len(metas))
        meta = metas[0]
        self.assertEqual('Jim Smith', meta['content'])
        self.assertEqual('Author', meta['name'])
        self.assertEqual('test', meta['scheme'])
        self.assertEqual('en-us', meta['lang'])
        self.assertEqual('en-us', meta['xml:lang'])
        meta = metas[1]
        self.assertEqual('Tue, 20 Aug 1996 14:25:27 GMT', meta['content'])
        self.assertEqual('Expires', meta['http-equiv'])

    def test_add_link_simple(self):
        req = Request()
        add_link(req, 'start', '/trac.cgi/wiki')
        self.assertEqual('/trac.cgi/wiki',
                         req.chrome['links']['start'][0]['href'])

    def test_add_link_advanced(self):
        req = Request()
        add_link(req, 'start', '/trac.cgi/wiki', 'Start page', 'text/html',
                 'home')
        link = req.chrome['links']['start'][0]
        self.assertEqual('/trac.cgi/wiki', link['href'])
        self.assertEqual('Start page', link['title'])
        self.assertEqual('text/html', link['type'])
        self.assertEqual('home', link['class'])

    def test_add_script(self):
        req = Request(base_path='/trac.cgi', href=Href('/trac.cgi'))
        add_script(req, 'common/js/trac.js')
        add_script(req, 'common/js/trac.js')
        add_script(req, 'http://example.com/trac.js')
        add_script(req, '//example.com/trac.js')
        add_script(req, '/dynamic.js')
        add_script(req, 'plugin/js/plugin.js')
        scripts = req.chrome['scripts']
        self.assertEqual(5, len(scripts))
        self.assertNotIn('type', scripts[0]['attrs'])
        self.assertEqual('/trac.cgi/chrome/common/js/trac.js',
                         scripts[0]['attrs']['src'])
        self.assertNotIn('type', scripts[1]['attrs'])
        self.assertEqual('http://example.com/trac.js',
                         scripts[1]['attrs']['src'])
        self.assertNotIn('type', scripts[2]['attrs'])
        self.assertEqual('//example.com/trac.js',
                         scripts[2]['attrs']['src'])
        self.assertEqual('/trac.cgi/dynamic.js',
                         scripts[3]['attrs']['src'])
        self.assertEqual('/trac.cgi/chrome/plugin/js/plugin.js',
                         scripts[4]['attrs']['src'])

    def test_add_script_data(self):
        req = MockRequest(self.env)
        add_script_data(req, {'var1': 1, 'var2': 'Testing'})
        add_script_data(req, var2='More testing', var3=3)
        self.assertEqual({'var1': 1, 'var2': 'More testing', 'var3': 3},
                         req.chrome['script_data'])

    def test_add_stylesheet(self):
        req = MockRequest(self.env)
        add_stylesheet(req, 'common/css/trac.css')
        add_stylesheet(req, 'common/css/trac.css')
        add_stylesheet(req, 'https://example.com/trac.css')
        add_stylesheet(req, '//example.com/trac.css')
        add_stylesheet(req, '/dynamic.css')
        add_stylesheet(req, 'plugin/css/plugin.css')
        links = req.chrome['links']['stylesheet']
        self.assertEqual(5, len(links))
        self.assertEqual('text/css', links[0]['type'])
        self.assertEqual('/trac.cgi/chrome/common/css/trac.css',
                         links[0]['href'])
        self.assertEqual('text/css', links[1]['type'])
        self.assertEqual('https://example.com/trac.css',
                         links[1]['href'])
        self.assertEqual('text/css', links[2]['type'])
        self.assertEqual('//example.com/trac.css',
                         links[2]['href'])
        self.assertEqual('/trac.cgi/dynamic.css',
                         links[3]['href'])
        self.assertEqual('/trac.cgi/chrome/plugin/css/plugin.css',
                         links[4]['href'])

    def test_add_stylesheet_media(self):
        req = Request(base_path='/trac.cgi', href=Href('/trac.cgi'))
        add_stylesheet(req, 'foo.css', media='print')
        links = req.chrome['links']['stylesheet']
        self.assertEqual(1, len(links))
        self.assertEqual('print', links[0]['media'])

    def test_add_warning_is_unique(self):
        req = MockRequest(self.env)
        message = random_sentence(5)
        add_warning(req, message)
        add_warning(req, message)
        self.assertEqual(1, len(req.chrome['warnings']))

    def test_add_notice_is_unique(self):
        req = MockRequest(self.env)
        message = random_sentence(5)
        add_notice(req, message)
        add_notice(req, message)
        self.assertEqual(1, len(req.chrome['notices']))

    def _test_add_message_escapes_markup(self, msgtype, add_fn):
        req = MockRequest(self.env)
        add_fn(req, 'Message with an "&"')
        add_fn(req, Exception("Exception message with an &"))
        add_fn(req, tag("Message with text ", tag.b("& markup")))
        add_fn(req, Markup("Markup <strong>message</strong>."))
        messages = req.chrome[msgtype]
        self.assertIn('Message with an "&amp;"', messages)
        self.assertIn("Exception message with an &amp;", messages)
        self.assertIn("Message with text <b>&amp; markup</b>", messages)
        self.assertIn("Markup <strong>message</strong>.", messages)

    def test_add_warning_escapes_markup(self):
        """Message text is escaped. Regression test for
        https://trac.edgewall.org/ticket/12285
        """
        self._test_add_message_escapes_markup('warnings', add_warning)

    def test_add_notice_escapes_markup(self):
        """Message text is escaped. Regression test for
        https://trac.edgewall.org/ticket/12285
        """
        self._test_add_message_escapes_markup('notices', add_notice)

    def test_htdocs_location(self):
        req = MockRequest(self.env)
        self.assertEqual('/trac.cgi/chrome/common/',
                         req.chrome['htdocs_location'])

    def test_logo(self):
        # Verify that no logo data is put in the HDF if no logo is configured
        req = MockRequest(self.env)
        self.env.config.set('header_logo', 'src', '')
        self.assertNotIn('src', req.chrome['logo'])
        self.assertNotIn('src_abs', req.chrome['logo'])

        # Test with a relative path to the logo image
        req = MockRequest(self.env)
        self.env.config.set('header_logo', 'src', 'foo.png')
        self.assertEqual('/trac.cgi/chrome/common/foo.png',
                         req.chrome['logo']['src'])
        self.assertEqual('http://example.org/trac.cgi/chrome/common/foo.png',
                         req.chrome['logo']['src_abs'])

        # Test with a location in project htdocs
        req = MockRequest(self.env)
        self.env.config.set('header_logo', 'src', 'site/foo.png')
        self.assertEqual('/trac.cgi/chrome/site/foo.png',
                         req.chrome['logo']['src'])
        self.assertEqual('http://example.org/trac.cgi/chrome/site/foo.png',
                         req.chrome['logo']['src_abs'])

        # Test with a server-relative path to the logo image
        req = MockRequest(self.env)
        self.env.config.set('header_logo', 'src', '/img/foo.png')
        self.assertEqual('/img/foo.png', req.chrome['logo']['src'])
        self.assertEqual('/img/foo.png', req.chrome['logo']['src_abs'])

        # Test with an absolute path to the logo image
        req = MockRequest(self.env)
        self.env.config.set('header_logo', 'src',
                            'http://www.example.org/foo.png')
        self.assertEqual('http://www.example.org/foo.png',
                         req.chrome['logo']['src'])
        self.assertEqual('http://www.example.org/foo.png',
                         req.chrome['logo']['src_abs'])

    def test_default_links(self):
        req = MockRequest(self.env)
        links = req.chrome['links']

        self.assertEqual('/trac.cgi/wiki', links['start'][0]['href'])
        self.assertEqual('/trac.cgi/search', links['search'][0]['href'])
        self.assertEqual('/trac.cgi/wiki/TracGuide', links['help'][0]['href'])
        self.assertEqual('/trac.cgi/chrome/common/css/trac.css',
                         links['stylesheet'][0]['href'])

    def test_icon_links(self):
        # No icon set in config, so no icon links
        req = MockRequest(self.env)
        self.env.config.set('project', 'icon', '')
        links = req.chrome['links']
        self.assertNotIn('icon', links)
        self.assertNotIn('shortcut icon', links)

        # Relative URL for icon config option
        req = MockRequest(self.env)
        self.env.config.set('project', 'icon', 'foo.ico')
        links = req.chrome['links']
        self.assertEqual('/trac.cgi/chrome/common/foo.ico',
                         links['icon'][0]['href'])
        self.assertNotIn('shortcut icon', links)

        # URL relative to the server root for icon config option
        req = MockRequest(self.env)
        self.env.config.set('project', 'icon', '/favicon.ico')
        links = req.chrome['links']
        self.assertEqual('/favicon.ico', links['icon'][0]['href'])
        self.assertNotIn('shortcut icon', links)

        # Absolute URL for icon config option
        req = MockRequest(self.env)
        self.env.config.set('project', 'icon',
                            'http://example.com/favicon.ico')
        links = req.chrome['links']
        self.assertEqual('http://example.com/favicon.ico',
                         links['icon'][0]['href'])
        self.assertNotIn('shortcut icon', links)

    def _get_jquery_ui_script_data(self, lc_time):
        req = MockRequest(self.env, lc_time=lc_time)
        Chrome(self.env).add_jquery_ui(req)
        return req.chrome['script_data']['jquery_ui']

    def test_add_jquery_ui_is_iso8601(self):
        data = self._get_jquery_ui_script_data('iso8601')
        self.assertIn({'value': -60, 'label': '-01:00'}, data['timezone_list'])
        self.assertIn({'value': 0, 'label': '+00:00'}, data['timezone_list'])
        self.assertIn({'value': 60, 'label': '+01:00'}, data['timezone_list'])

    def test_add_jquery_ui_default_format(self):
        data = self._get_jquery_ui_script_data(locale_en)
        self.assertIsNone(data['timezone_list'])

    def test_invalid_default_dateinfo_format_raises_exception(self):
        self.env.config.set('trac', 'default_dateinfo_format', 'ābšolute')

        self.assertEqual('ābšolute',
                         self.env.config.get('trac', 'default_dateinfo_format'))
        with self.assertRaises(ConfigurationError):
            Chrome(self.env).default_dateinfo_format

    def test_add_jquery_ui_first_week_day(self):
        def first_week_day(locale, lc_time, languages):
            chrome = Chrome(self.env)
            languages = ','.join(languages) if languages else ''
            req = MockRequest(self.env, locale=locale, lc_time=lc_time,
                              language=languages)
            chrome.add_jquery_ui(req)
            return req.chrome['script_data']['jquery_ui']['first_week_day']

        # Babel is unavailable
        self.assertEqual(0, first_week_day(None, None, None))
        self.assertEqual(1, first_week_day(None, 'iso8601', None))
        if locale_en:
            # We expect the following aliases
            from babel.core import LOCALE_ALIASES, Locale
            self.assertEqual('ja_JP', LOCALE_ALIASES['ja'])
            self.assertEqual('de_DE', LOCALE_ALIASES['de'])
            self.assertEqual('fr_FR', LOCALE_ALIASES['fr'])

            self.assertEqual(0, first_week_day(locale_en, locale_en, []))
            self.assertEqual(1, first_week_day(locale_en, 'iso8601', []))
            ja = Locale.parse('ja')
            self.assertEqual(0, first_week_day(ja, ja, []))
            self.assertEqual(0, first_week_day(ja, ja, ['ja', 'ja-jp']))
            de = Locale.parse('de')
            self.assertEqual(1, first_week_day(de, de, []))
            self.assertEqual(1, first_week_day(de, de, ['de', 'de-de']))
            fr = Locale.parse('fr')
            self.assertEqual(1, first_week_day(fr, fr, []))
            self.assertEqual(1, first_week_day(fr, fr, ['fr', 'fr-fr']))
            self.assertEqual(0, first_week_day(fr, fr, ['fr', 'fr-ca']))
            # invalid locale identifier (#12408)
            self.assertEqual(1, first_week_day(fr, fr, ['fr', 'fr-']))
            self.assertEqual(0, first_week_day(fr, fr, ['fr', 'fr-', 'fr-ca']))

    def test_add_jquery_ui_timezone_list_has_default_timezone(self):
        chrome = Chrome(self.env)
        gmt07b = timezone('GMT -7:00')
        gmt04a = timezone('GMT +4:00')

        def verify_tzprops(lc_time, tz, tz_default, tz_label):
            req = MockRequest(self.env, locale=locale_en, lc_time=lc_time,
                              tz=tz)
            chrome.add_jquery_ui(req)
            data = req.chrome['script_data']['jquery_ui']
            self.assertEqual(tz_default, data['default_timezone'])
            if tz_label is not None:
                self.assertIn({'value': tz_default, 'label': tz_label},
                              data['timezone_list'])

        verify_tzprops('iso8601', utc, 0, '+00:00')
        verify_tzprops(locale_en, utc, 0, None)
        verify_tzprops('iso8601', gmt07b, -420, '-07:00')
        verify_tzprops(locale_en, gmt07b, -420, None)
        verify_tzprops('iso8601', gmt04a, 240, '+04:00')
        verify_tzprops(locale_en, gmt04a, 240, None)
        if pytz:
            # must use timezones which does not use DST
            guam = timezone('Pacific/Guam')
            monrovia = timezone('Africa/Monrovia')
            panama = timezone('America/Panama')
            verify_tzprops('iso8601', guam, 600, '+10:00')
            verify_tzprops(locale_en, guam, 600, None)
            verify_tzprops('iso8601', monrovia, 0, '+00:00')
            verify_tzprops(locale_en, monrovia, 0, None)
            verify_tzprops('iso8601', panama, -300, '-05:00')
            verify_tzprops(locale_en, panama, -300, None)

    def test_cc_list(self):
        """Split delimited string to a list of email addresses."""
        chrome = Chrome(self.env)
        cc_field1 = 'user1@abc.com,user2@abc.com, user3@abc.com'
        cc_field2 = 'user1@abc.com;user2@abc.com; user3@abc.com'
        cc_field3 = ' user1@abc.com,  user2@abc.com;user3@abc.com  '
        expected = ['user1@abc.com', 'user2@abc.com', 'user3@abc.com']
        self.assertEqual(expected, chrome.cc_list(cc_field1))
        self.assertEqual(expected, chrome.cc_list(cc_field2))
        self.assertEqual(expected, chrome.cc_list(cc_field3))

    def test_cc_list_is_empty(self):
        """Empty list is returned when input is `None` or empty."""
        chrome = Chrome(self.env)
        self.assertEqual([], chrome.cc_list(None))
        self.assertEqual([], chrome.cc_list(''))
        self.assertEqual([], chrome.cc_list([]))


class ChromeTestCase2(unittest.TestCase):

    def setUp(self):
        self.env = EnvironmentStub(path=mkdtemp())
        self.chrome = Chrome(self.env)

    def tearDown(self):
        self.env.reset_db_and_disk()

    def test_permission_requestor(self):
        self.assertIn('EMAIL_VIEW', PermissionSystem(self.env).get_actions())

    def test_malicious_filename_raises(self):
        req = MockRequest(self.env, path_info='/chrome/site/../conf/trac.ini')
        self.assertTrue(self.chrome.match_request(req))
        with self.assertRaises(TracError):
            self.chrome.process_request(req)

    def test_empty_shared_htdocs_dir_raises_file_not_found(self):
        req = MockRequest(self.env, path_info='/chrome/shared/trac_logo.png')
        self.assertEqual('', self.chrome.shared_htdocs_dir)
        self.assertTrue(self.chrome.match_request(req))
        from trac.web.api import HTTPNotFound
        with self.assertRaises(HTTPNotFound):
            self.chrome.process_request(req)

    def test_shared_htdocs_dir_file_is_found(self):
        from trac.web.api import RequestDone
        def send_file(path, mimetype):
            raise RequestDone
        req = Request(path_info='/chrome/shared/trac_logo.png',
                      send_file=send_file)

        shared_htdocs_dir = os.path.join(self.env.path, 'chrome', 'shared')
        os.makedirs(shared_htdocs_dir)
        create_file(os.path.join(shared_htdocs_dir, 'trac_logo.png'))
        self.env.config.set('inherit', 'htdocs_dir', shared_htdocs_dir)
        self.assertTrue(self.chrome.match_request(req))
        with self.assertRaises(RequestDone):
            self.chrome.process_request(req)


class NavigationContributorTestCase(unittest.TestCase):

    navigation_contributors = []

    @classmethod
    def setUpClass(cls):
        class TestNavigationContributor1(Component):
            implements(INavigationContributor)

            def get_active_navigation_item(self, req):
                return 'test1'

            def get_navigation_items(self, req):
                yield 'mainnav', 'test1', 'Test 1'

        class TestNavigationContributor2(Component):
            implements(INavigationContributor)

            def get_active_navigation_item(self, req):
                return 'test2'

            def get_navigation_items(self, req):
                yield 'mainnav', 'test2', \
                      tag.a('Test 2', href='test2', target='blank')

        class TestNavigationContributor3(Component):
            implements(INavigationContributor)

            def get_active_navigation_item(self, req):
                return 'test3'

            def get_navigation_items(self, req):
                yield 'mainnav', 'test3', \
                      tag.a('Test 3', href='test3', target='blank')

        cls.navigation_contributors = [TestNavigationContributor1,
                                       TestNavigationContributor2,
                                       TestNavigationContributor3]

    def setUp(self):
        self.env = EnvironmentStub(enable=self.navigation_contributors)

    @classmethod
    def tearDownClass(cls):
        from trac.core import ComponentMeta
        for class_ in cls.navigation_contributors:
            ComponentMeta.deregister(class_)

    def _get_navigation_item(self, items, name):
        for item in items:
            if item['name'] == name:
                return item
        return {}

    def test_nav_contributor(self):
        req = MockRequest(self.env)
        nav = req.chrome['nav']
        self.assertEqual({'name': 'test1', 'label': 'Test 1',
                          'active': False}, nav['mainnav'][0])

    def test_nav_contributor_active(self):
        req = MockRequest(self.env)
        handler = self.navigation_contributors[0](self.env)
        nav = Chrome(self.env).prepare_request(req, handler)['nav']
        self.assertEqual({'name': 'test1', 'label': 'Test 1', 'active': True},
                         nav['mainnav'][0])

    def test_navigation_item_customization(self):
        req = MockRequest(self.env)
        self.env.config.set('mainnav', 'test2.href', 'testtwo')
        self.env.config.set('mainnav', 'test3.label', 'Test Three')
        self.env.config.set('mainnav', 'test3.href', 'testthree')

        mainnav = req.chrome['nav']['mainnav']

        item = self._get_navigation_item(mainnav, 'test1')
        self.assertEqual('Test 1', item['label'])
        item = self._get_navigation_item(mainnav, 'test2')
        self.assertEqual(str(tag.a('Test 2', href='testtwo', target='blank')),
                         str(item['label']))
        item = self._get_navigation_item(mainnav, 'test3')
        self.assertEqual(str(tag.a('Test Three', href='testthree',
                                   target='blank')),
                         str(item['label']))

    def test_attributes_preserved_in_navigation_item(self):
        req = MockRequest(self.env)
        self.env.config.set('mainnav', 'test2.label', 'Test Two')
        self.env.config.set('mainnav', 'test3.label', 'Test Three')
        self.env.config.set('mainnav', 'test3.href', 'testthree')

        mainnav = req.chrome['nav']['mainnav']

        item = self._get_navigation_item(mainnav, 'test2')
        self.assertEqual(str(tag.a('Test Two', href='test2', target='blank')),
                         str(item['label']))
        item = self._get_navigation_item(mainnav, 'test3')
        self.assertEqual(str(tag.a('Test Three', href='testthree',
                                   target='blank')),
                         str(item['label']))


class NavigationCustomizationTestCase(unittest.TestCase):

    navigation_contributors = []

    @classmethod
    def setUpClass(cls):
        class TestNavigationContributor1(Component):
            implements(INavigationContributor)

            def get_active_navigation_item(self, req):
                return 'test1'

            def get_navigation_items(self, req):
                yield 'metanav', 'test1', \
                      tag.a('Test 1', href=req.href.test('1'))

        class TestNavigationContributor2(Component):
            implements(INavigationContributor, IPermissionRequestor)

            def get_active_navigation_item(self, req):
                return 'test2'

            def get_permission_actions(self):
                return ['TEST2_VIEW']

            def get_navigation_items(self, req):
                if 'TEST2_VIEW' in req.perm:
                    yield 'metanav', 'test2', \
                          tag.a('Test 2', href=req.href.test('2'))

        class TestNavigationContributor3(Component):
            implements(INavigationContributor, IPermissionRequestor,
                       IRequestHandler)

            def match_request(self, req):
                return req.path_info == '/test/3'

            def process_request(self, req):
                pass

            def get_permission_actions(self):
                return ['TEST3_VIEW']

            def get_active_navigation_item(self, req):
                return 'test3'

            def get_navigation_items(self, req):
                yield 'mainnav', 'test3', \
                      tag.a('Test 3', href=req.href.test('3'))

        cls.navigation_contributors = [TestNavigationContributor1,
                                       TestNavigationContributor2,
                                       TestNavigationContributor3]

    def setUp(self):
        self.env = EnvironmentStub(enable=self.navigation_contributors +
                                          ['trac.perm.*'])
        self.env.config.set('trac', 'permission_policies',
                            'DefaultPermissionPolicy')

    @classmethod
    def tearDownClass(cls):
        from trac.core import ComponentMeta
        for class_ in cls.navigation_contributors:
            ComponentMeta.deregister(class_)

    def test_explicit_ordering(self):
        """Ordering is explicitly specified."""
        req = MockRequest(self.env)
        self.env.config.set('metanav', 'test1.order', 2)
        self.env.config.set('metanav', 'test2.order', 1)
        metanav = req.chrome['nav']['metanav']
        self.assertEqual('test2', metanav[0]['name'])
        self.assertEqual('test1', metanav[1]['name'])

    def test_partial_explicit_ordering(self):
        """Ordering for one item is explicitly specified."""
        req = MockRequest(self.env)
        self.env.config.set('metanav', 'test2.order', 1)
        metanav = req.chrome['nav']['metanav']
        self.assertEqual('test2', metanav[0]['name'])
        self.assertEqual('test1', metanav[1]['name'])

    def test_implicit_ordering(self):
        """When not specified, ordering is alphabetical."""
        req = MockRequest(self.env)
        self.env.config.set('metanav', 'foo.order', 1)
        self.env.config.set('metanav', 'bar.order', 2)
        metanav = req.chrome['nav']['metanav']
        self.assertEqual('test1', metanav[0]['name'])
        self.assertEqual('test2', metanav[1]['name'])

    def test_add_new_item(self):
        """New items added to the main nav."""
        req = MockRequest(self.env)
        self.env.config.set('metanav', 'tracguide', 'enabled')
        self.env.config.set('metanav', 'tracguide.href', '/wiki/TracGuide')
        self.env.config.set('metanav', 'tracguide.label', 'Trac Guide')
        self.env.config.set('mainnav', 'google', 'enabled')
        self.env.config.set('mainnav', 'google.href', 'https://google.com')

        items = req.chrome['nav']
        mainnav = items['mainnav']

        self.assertEqual('google', mainnav[0]['name'])
        self.assertEqual('<a href="https://google.com">google</a>',
                         str(mainnav[0]['label']))
        self.assertEqual('tracguide', items['metanav'][2]['name'])
        self.assertEqual('<a href="/trac.cgi/wiki/TracGuide">Trac Guide</a>',
                         str(items['metanav'][2]['label']))

    def test_move_metanav_to_mainnav(self):
        """Move items between metanav and mainnav."""
        req = MockRequest(self.env)
        self.env.config.set('metanav', 'test1.order', 1)
        self.env.config.set('metanav', 'test1.perm', 'TRAC_ADMIN')
        self.env.config.set('mainnav', 'test1', 'enabled')
        self.env.config.set('mainnav', 'test1.order', 2)
        self.env.config.set('mainnav', 'test1.permission', 'TEST3_VIEW')
        self.env.config.set('mainnav', 'test2.order', 1)
        self.env.config.set('mainnav', 'test3.order', 1)

        items = req.chrome['nav']
        mainnav = items['mainnav']
        metanav = items['metanav']

        self.assertEqual(1, len(metanav))
        self.assertEqual('test2', str(metanav[0]['name']))
        self.assertEqual('<a href="/trac.cgi/test/2">Test 2</a>',
                         str(metanav[0]['label']))
        self.assertEqual(2, len(mainnav))
        self.assertNotIn('test1', metanav)
        self.assertEqual('test3', str(mainnav[0]['name']))
        self.assertEqual('<a href="/trac.cgi/test/3">Test 3</a>',
                         str(mainnav[0]['label']))
        self.assertFalse(mainnav[0]['active'])
        self.assertEqual('test1', str(mainnav[1]['name']))
        self.assertEqual('<a href="/trac.cgi/test/1">Test 1</a>',
                         str(mainnav[1]['label']))

    def test_disable_items(self):
        """Disable navigation items."""
        req = MockRequest(self.env)
        self.env.config.set('metanav', 'test1', 'disabled')
        self.env.config.set('mainnav', 'test3', 'disabled')
        self.env.config.set('mainnav', 'test4', 'disabled')

        items = req.chrome['nav']
        mainnav = items['mainnav']
        metanav = items['metanav']

        self.assertEqual(1, len(metanav))
        self.assertEqual('test2', str(metanav[0]['name']))
        self.assertEqual([], mainnav)

    def test_permission_attribute(self):
        """The `permission` attribute controls visibility of the item."""
        ps = PermissionSystem(self.env)
        ps.grant_permission('user1', 'TEST2_VIEW')
        ps.grant_permission('user1', 'TEST3_VIEW')
        self.env.config.set('mainnav', 'test2', 'enabled')
        self.env.config.set('mainnav', 'test3.permission', 'TEST3_VIEW')
        self.env.config.set('mainnav', 'test4', 'enabled')
        self.env.config.set('mainnav', 'test4.permission', 'TEST4_VIEW')

        req = MockRequest(self.env, authname='user1')
        items = req.chrome['nav']
        mainnav = items['mainnav']
        self.assertEqual(2, len(mainnav))
        self.assertIn('test2', mainnav[0]['name'])
        self.assertIn('test3', mainnav[1]['name'])

        req = MockRequest(self.env, authname='user2')
        items = req.chrome['nav']
        mainnav = items['mainnav']
        self.assertEqual([], mainnav)

    def test_active_highlighting(self):
        """The navigation item matching the path is highlighted as active.
        """
        self.env.config.set('mainnav', 'test4', 'enabled')
        self.env.config.set('mainnav', 'test4.href', '/test/3/1')
        chrome = Chrome(self.env)
        handler = self.navigation_contributors[2](self.env)

        req = MockRequest(self.env, path_info='/test/3')
        items = chrome.prepare_request(req, handler)['nav']
        mainnav = items['mainnav']
        self.assertEqual(2, len(mainnav))
        self.assertEqual('test3', str(mainnav[0]['name']))
        self.assertTrue(mainnav[0]['active'])
        self.assertEqual('test4', str(mainnav[1]['name']))
        self.assertFalse(mainnav[1]['active'])

        req = MockRequest(self.env, path_info='/test/3/1')
        items = chrome.prepare_request(req, handler)['nav']
        mainnav = items['mainnav']
        self.assertEqual(2, len(mainnav))
        self.assertEqual('test3', str(mainnav[0]['name']))
        self.assertFalse(mainnav[0]['active'])
        self.assertEqual('test4', str(mainnav[1]['name']))
        self.assertTrue(mainnav[1]['active'])


@unittest.skipUnless(LazyProxy, 'Babel unavailable')
class NavigationLazyProxyTestCase(unittest.TestCase):

    navigation_contributors = []

    @classmethod
    def setUpClass(cls):
        class TestNavigationContributor1(Component):
            implements(INavigationContributor)

            def get_active_navigation_item(self, req):
                return None

            def get_navigation_items(self, req):
                yield 'metanav', 'test1', LazyProxy(lambda: 'Test 1')
                yield 'metanav', 'test2', LazyProxy(tag, 'Test ', tag.b('2'))

        cls.navigation_contributors = [TestNavigationContributor1]

    def setUp(self):
        self.env = EnvironmentStub(enable=self.navigation_contributors)

    @classmethod
    def tearDownClass(cls):
        from trac.core import ComponentMeta
        for class_ in cls.navigation_contributors:
            ComponentMeta.deregister(class_)

    def test_lazyproxy_items(self):
        req = MockRequest(self.env)
        metanav = req.chrome['nav']['metanav']
        self.assertEqual({'name': 'test1', 'label': 'Test 1',
                          'active': False}, metanav[0])
        self.assertEqual('test2', metanav[1]['name'])
        self.assertEqual('Test <b>2</b>', str(metanav[1]['label']))
        self.assertEqual(False, metanav[1]['active'])
        self.assertEqual(2, len(metanav))


class FormatAuthorTestCase(unittest.TestCase):

    def setUp(self):
        self.env = EnvironmentStub(enable=['trac.web.chrome.*',
                                           'trac.perm.*',
                                           'tracopt.perm.authz_policy'])
        self.env.config.set('trac', 'permission_policies',
                            'AuthzPolicy, DefaultPermissionPolicy')
        fd, self.authz_file = tempfile.mkstemp()
        with os.fdopen(fd, 'w') as f:
            f.write(textwrap.dedent("""\
                [wiki:WikiStart]
                user2 = EMAIL_VIEW
                [wiki:TracGuide]
                user2 =
                """))
        PermissionSystem(self.env).grant_permission('user1', 'EMAIL_VIEW')
        self.env.config.set('authz_policy', 'authz_file', self.authz_file)

    def tearDown(self):
        os.remove(self.authz_file)

    def test_subject_is_anonymous(self):
        format_author = Chrome(self.env).format_author
        self.assertEqual('anonymous', format_author(None, 'anonymous'))

    def test_subject_is_none(self):
        format_author = Chrome(self.env).format_author
        self.assertEqual('(none)', format_author(None, None))

    def test_actor_has_email_view(self):
        req = MockRequest(self.env, authname='user1')
        author = Chrome(self.env).format_author(req, 'user@domain.com')
        self.assertEqual('user@domain.com', author)

    def test_actor_no_email_view(self):
        req = MockRequest(self.env, authname='user2')
        author = Chrome(self.env).format_author(req, 'user@domain.com')
        self.assertEqual('user@\u2026', author)

    def test_actor_no_email_view_show_email_addresses(self):
        self.env.config.set('trac', 'show_email_addresses', True)
        req = MockRequest(self.env, authname='user2')
        author = Chrome(self.env).format_author(req, 'user@domain.com')
        self.assertEqual('user@domain.com', author)

    def test_actor_no_email_view_no_req(self):
        author = Chrome(self.env).format_author(None, 'user@domain.com')
        self.assertEqual('user@\u2026', author)

    def test_actor_has_email_view_for_resource(self):
        format_author = Chrome(self.env).format_author
        req = MockRequest(self.env, authname='user2')
        resource = Resource('wiki', 'WikiStart')
        author = format_author(req, 'user@domain.com', resource)
        self.assertEqual('user@domain.com', author)

    def test_actor_has_email_view_for_resource_negative(self):
        format_author = Chrome(self.env).format_author
        req = MockRequest(self.env, authname='user2')
        resource = Resource('wiki', 'TracGuide')
        author = format_author(req, 'user@domain.com', resource)
        self.assertEqual('user@\u2026', author)

    def test_show_full_names_true(self):
        format_author = Chrome(self.env).format_author
        self.env.config.set('trac', 'show_full_names', True)
        self.env.insert_users([
            ('user1', 'User One', 'user1@example.org'),
            ('user2', None, None)
        ])

        self.assertEqual('User One', format_author(None, 'user1'))
        self.assertEqual('user2', format_author(None, 'user2'))

    def test_show_full_names_false(self):
        format_author = Chrome(self.env).format_author
        self.env.config.set('trac', 'show_full_names', False)

        self.assertEqual('user1', format_author(None, 'user1'))
        self.assertEqual('user2', format_author(None, 'user2'))

    def test_show_email_true(self):
        format_author = Chrome(self.env).format_author
        req = MockRequest(self.env, authname='user2')

        author = format_author(None, 'user@domain.com', show_email=True)
        self.assertEqual('user@domain.com', author)
        author = format_author(req, 'user@domain.com', show_email=True)
        self.assertEqual('user@domain.com', author)

    def test_show_email_false(self):
        format_author = Chrome(self.env).format_author
        req = MockRequest(self.env, authname='user1')

        author = format_author(None, 'user@domain.com', show_email=False)
        self.assertEqual('user@\u2026', author)
        author = format_author(req, 'user@domain.com', show_email=False)
        self.assertEqual('user@\u2026', author)

    def test_show_full_names_true_actor_has_email_view(self):
        format_author = Chrome(self.env).format_author
        self.env.config.set('trac', 'show_full_names', True)
        self.env.insert_users([
            ('user1', 'User One', 'user1@example.org'),
            ('user2', None, None)
        ])

        self.assertEqual('User One', format_author(None, 'user1'))
        self.assertEqual('user2', format_author(None, 'user2'))

    def test_show_full_names_false_actor_has_email_view(self):
        req = MockRequest(self.env, authname='user1')
        format_author = Chrome(self.env).format_author
        self.env.config.set('trac', 'show_full_names', False)

        self.assertEqual('user1', format_author(req, 'user1'))
        self.assertEqual('user2', format_author(req, 'user2'))

    def test_show_email_addresses_true(self):
        req = MockRequest(self.env)
        format_author = Chrome(self.env).format_author
        self.env.config.set('trac', 'show_email_addresses', True)

        self.assertEqual('user3@example.org',
                         format_author(None, 'user3@example.org'))
        self.assertEqual('user3@example.org',
                         format_author(req, 'user3@example.org'))

    def test_show_email_addresses_false(self):
        req = MockRequest(self.env)
        format_author = Chrome(self.env).format_author
        self.env.config.set('trac', 'show_email_addresses', False)

        self.assertEqual('user3@\u2026',
                         format_author(None, 'user3@example.org'))
        self.assertEqual('user3@example.org',
                         format_author(req, 'user3@example.org'))

    def test_format_emails(self):
        format_emails = Chrome(self.env).format_emails
        to_format = 'user1@example.org, user2; user3@example.org'

        self.assertEqual('user1@\u2026, user2, user3@\u2026',
                         format_emails(None, to_format))

    def test_format_emails_actor_has_email_view(self):
        req = MockRequest(self.env, authname='user1')
        context = web_context(req)
        format_emails = Chrome(self.env).format_emails
        to_format = 'user1@example.org, user2; user3@example.org'

        self.assertEqual('user1@example.org, user2, user3@example.org',
                         format_emails(context, to_format))

    def test_format_emails_actor_no_email_view(self):
        req = MockRequest(self.env, authname='user2')
        context = web_context(req)
        format_emails = Chrome(self.env).format_emails
        to_format = 'user1@example.org, user2; user3@example.org'

        self.assertEqual('user1@\u2026, user2, user3@\u2026',
                         format_emails(context, to_format))


class AuthorInfoTestCase(unittest.TestCase):

    def setUp(self):
        self.env = EnvironmentStub(enable=['trac.web.chrome.*',
                                           'trac.perm.*',
                                           'tracopt.perm.authz_policy'])
        self.env.config.set('trac', 'permission_policies',
                            'AuthzPolicy, DefaultPermissionPolicy')
        fd, self.authz_file = tempfile.mkstemp()
        with os.fdopen(fd, 'w') as f:
            f.write(textwrap.dedent("""\
                [wiki:WikiStart]
                user2 = EMAIL_VIEW
                [wiki:TracGuide]
                user2 =
                """))
        PermissionSystem(self.env).grant_permission('user1', 'EMAIL_VIEW')
        self.env.config.set('authz_policy', 'authz_file', self.authz_file)

    def tearDown(self):
        os.remove(self.authz_file)

    def test_subject_is_anonymous(self):
        chrome = Chrome(self.env)
        req = MockRequest(self.env)
        self.assertEqual('<span class="trac-author-anonymous">anonymous</span>',
                         str(chrome.authorinfo(req, 'anonymous')))
        self.assertEqual('<span class="trac-author-anonymous">anonymous</span>',
                         str(chrome.authorinfo_short('anonymous')))

    def test_subject_is_none(self):
        chrome = Chrome(self.env)
        req = MockRequest(self.env)
        self.assertEqual('<span class="trac-author">(none)</span>',
                         str(chrome.authorinfo(req, '(none)')))
        self.assertEqual('<span class="trac-author-none">(none)</span>',
                         str(chrome.authorinfo(req, None)))
        self.assertEqual('<span class="trac-author-none">(none)</span>',
                         str(chrome.authorinfo(req, '')))
        self.assertEqual('<span class="trac-author">(none)</span>',
                         str(chrome.authorinfo_short('(none)')))
        self.assertEqual('<span class="trac-author-none">(none)</span>',
                         str(chrome.authorinfo_short(None)))
        self.assertEqual('<span class="trac-author-none">(none)</span>',
                         str(chrome.authorinfo_short('')))

    def test_actor_has_email_view(self):
        chrome = Chrome(self.env)
        req = MockRequest(self.env, authname='user1')
        self.assertEqual('<span class="trac-author">user@domain.com</span>',
                         str(chrome.authorinfo(req, 'user@domain.com')))
        self.assertEqual('<span class="trac-author">User One &lt;user@example.org&gt;</span>',
                         str(chrome.authorinfo(req, 'User One <user@example.org>')))
        self.assertEqual('<span class="trac-author">user</span>',
                         str(chrome.authorinfo_short('User One <user@example.org>')))
        self.assertEqual('<span class="trac-author">user</span>',
                         str(chrome.authorinfo_short('user@example.org')))

    def test_actor_no_email_view(self):
        req = MockRequest(self.env, authname='user2')
        authorinfo = Chrome(self.env).authorinfo
        self.assertEqual('<span class="trac-author">user@\u2026</span>',
                         str(authorinfo(req, 'user@domain.com')))
        self.assertEqual('<span class="trac-author">User One &lt;user@\u2026&gt;</span>',
                         str(authorinfo(req, 'User One <user@domain.com>')))

    def test_actor_no_email_view_show_email_addresses(self):
        self.env.config.set('trac', 'show_email_addresses', True)
        req = MockRequest(self.env, authname='user2')
        authorinfo = Chrome(self.env).authorinfo
        self.assertEqual('<span class="trac-author">user@domain.com</span>',
                         str(authorinfo(req, 'user@domain.com')))
        self.assertEqual('<span class="trac-author">User One &lt;user@domain.com&gt;</span>',
                         str(authorinfo(req, 'User One <user@domain.com>')))

    def test_actor_no_email_view_no_req(self):
        authorinfo = Chrome(self.env).authorinfo
        self.assertEqual('<span class="trac-author">user@\u2026</span>',
                         str(authorinfo(None, 'user@domain.com')))
        self.assertEqual('<span class="trac-author">User One &lt;user@\u2026&gt;</span>',
                         str(authorinfo(None, 'User One <user@domain.com>')))

    def test_actor_has_email_view_for_resource(self):
        authorinfo = Chrome(self.env).authorinfo
        authorinfo_short = Chrome(self.env).authorinfo_short
        req = MockRequest(self.env, authname='user2')
        resource = Resource('wiki', 'WikiStart')
        authorinfo = authorinfo(req, 'user@domain.com', resource=resource)
        author_short = authorinfo_short('user@domain.com')
        self.assertEqual('<span class="trac-author">user@domain.com</span>',
                         str(authorinfo))
        self.assertEqual('<span class="trac-author">user</span>',
                         str(author_short))

    def test_actor_has_email_view_for_resource_negative(self):
        authorinfo = Chrome(self.env).authorinfo
        authorinfo_short = Chrome(self.env).authorinfo_short
        req = MockRequest(self.env, authname='user2')
        resource = Resource('wiki', 'TracGuide')
        author = authorinfo(req,  'user@domain.com', resource=resource)
        author_short = authorinfo_short('user@domain.com')
        self.assertEqual('<span class="trac-author">user@\u2026</span>',
                         str(author))
        self.assertEqual('<span class="trac-author">user</span>',
                         str(author_short))


class ChromeTemplateRenderingTestCase(unittest.TestCase):

    filename = 'test_chrome.html'

    template = textwrap.dedent("""\
        <!DOCTYPE html>
        <html>
        <body>
        %s
        </body>
        </html>
        """)

    def setUp(self):
        self.env = EnvironmentStub(path=mkdtemp())
        os.mkdir(self.env.templates_dir)
        self.filepath = os.path.join(self.env.templates_dir, self.filename)
        self.chrome = Chrome(self.env)

    def tearDown(self):
        self.env.reset_db_and_disk()

    def _create_template(self, body):
        create_file(self.filepath, self.template % body)

    def test_load_template(self):
        self._create_template('<h1>${greeting}</h1>')
        t1 = self.chrome.load_template(self.filename)
        self.assertTrue(isinstance(t1, jinja2.Template))
        t1_text = self.chrome.load_template(self.filename, text=True)
        self.assertTrue(isinstance(t1_text, jinja2.Template))
        # testing template cache
        t2 = self.chrome.load_template(self.filename)
        t2_text = self.chrome.load_template(self.filename, text=True)
        self.assertIs(t1, t2)
        self.assertIs(t1_text, t2_text)
        self.assertIsNot(t1, t1_text)

    def test_render_template_string(self):
        self._create_template('<h1>${greeting}</h1>')
        t = self.chrome.load_template(self.filename)
        self.assertIsNotNone(t)
        t_text = self.chrome.load_template(self.filename, text=True)
        self.assertIsNotNone(t_text)
        data = {'greeting': "Hell&ö"}
        content = self.chrome.render_template_string(t, data)
        self.assertIsInstance(content, Markup)
        self.assertEqual(textwrap.dedent("""\
            <!DOCTYPE html>
            <html>
            <body>
            <h1>Hell&amp;ö</h1>
            </body>
            </html>"""), content)
        content_text = self.chrome.render_template_string(t_text, data,
                                                          text=True)
        self.assertFalse(isinstance(content_text, Markup))
        self.assertIsInstance(content_text, str)
        self.assertEqual(textwrap.dedent("""\
            <!DOCTYPE html>
            <html>
            <body>
            <h1>Hell&ö</h1>
            </body>
            </html>"""), content_text)

    def test_render_template(self):
        self._create_template('<h1>${greeting}</h1>')
        data = {'greeting': "Hell&ö"}
        content = self.chrome.render_template(MockRequest(self.env),
                                              self.filename, data,
                                              {'fragment': True})
        self.assertIsInstance(content, bytes)
        self.assertEqual(textwrap.dedent("""\
            <!DOCTYPE html>
            <html>
            <body>
            <h1>Hell&amp;ö</h1>
            </body>
            </html>""").encode('utf-8'), content)

    def test_pretty_dateinfo(self):
        self._create_template(textwrap.dedent("""\
            <ul>
            <li>${pretty_dateinfo(None)}</li>
            <li>${pretty_dateinfo(dt)}</li>
            <li>${pretty_dateinfo(dt, format='relative')}</li>
            <li>${pretty_dateinfo(dt, format='date')}</li>
            <li>${pretty_dateinfo(dt, format='datetime')}</li>
            <li>${pretty_dateinfo(dt, format='date', dateonly=True)}</li>
            <li>${pretty_dateinfo(dt, format='datetime', dateonly=True)}</li>
            </ul>"""))
        data = {'dt': datetime.datetime(2007, 7, 1, 12, 34, 56, 987654, utc)}
        content = self.chrome.render_template(MockRequest(self.env),
                                              self.filename, data,
                                              {'fragment': True})

        self.assertIsInstance(content, bytes)
        content = str(content, 'utf-8')
        if has_babel:
            self.assertRegex(content, textwrap.dedent("""\
                <!DOCTYPE html>
                <html>
                <body>
                <ul>
                <li></li>
                <li><span title="Jul 1, 2007, 12:34:56 PM">[0-9]+ years ago</span></li>
                <li><span title="Jul 1, 2007, 12:34:56 PM">[0-9]+ years ago</span></li>
                <li><span title="[0-9]+ years ago">on Jul 1, 2007</span></li>
                <li><span title="[0-9]+ years ago">on Jul 1, 2007 at 12:34:56 PM</span></li>
                <li><span title="[0-9]+ years ago">Jul 1, 2007</span></li>
                <li><span title="[0-9]+ years ago">Jul 1, 2007, 12:34:56 PM</span></li>
                </ul>
                </body>
                </html>"""), content)
        else:
            self.assertRegex(content, textwrap.dedent("""\
                <!DOCTYPE html>
                <html>
                <body>
                <ul>
                <li></li>
                <li><span title="07/01/(20)?07 12:34:56">[0-9]+ years ago</span></li>
                <li><span title="07/01/(20)?07 12:34:56">[0-9]+ years ago</span></li>
                <li><span title="[0-9]+ years ago">on 07/01/(20)?07</span></li>
                <li><span title="[0-9]+ years ago">on 07/01/(20)?07 at 12:34:56</span></li>
                <li><span title="[0-9]+ years ago">07/01/(20)?07</span></li>
                <li><span title="[0-9]+ years ago">07/01/(20)?07 12:34:56</span></li>
                </ul>
                </body>
                </html>"""), content)

    def test_render_template_late_data(self):
        def fn():
            add_stylesheet(req, 'common/css/blahblah.css')
            add_script(req, 'common/js/blahblah.js')
            add_script_data(req, blahblah=42)
            return 'blahblah'

        template = textwrap.dedent("""\
            # extends 'layout.html'
            <!DOCTYPE html>
            <html>
              <body>
                # block content
                <div>${fn()}</div>
                ${ super() }
                # endblock content
              </body>
            </html>
            """)
        filename = 'test_render_template_late_data.html'
        filepath = os.path.join(self.env.templates_dir, filename)
        create_file(filepath, template)
        req = MockRequest(self.env)
        data = {'fn': fn}
        content = self.chrome.render_template(req, filename, data,
                                              {'fragment': True})
        self.assertIsInstance(content, bytes)
        self.assertIn(b'<div>blahblah</div>', content)
        self.assertIn(b' jQuery.loadScript("/trac.cgi/chrome/'
                      b'common/js/blahblah.js", "");', content)
        self.assertIn(b' jQuery.loadStyleSheet("/trac.cgi/chrome/'
                      b'common/css/blahblah.css", "text/css");', content)
        self.assertIn(b' var blahblah=42;', content)


def test_suite():
    suite = unittest.TestSuite()
    suite.addTest(makeSuite(ChromeTestCase))
    suite.addTest(makeSuite(ChromeTestCase2))
    suite.addTest(makeSuite(NavigationContributorTestCase))
    suite.addTest(makeSuite(NavigationCustomizationTestCase))
    suite.addTest(makeSuite(NavigationLazyProxyTestCase))
    suite.addTest(makeSuite(FormatAuthorTestCase))
    suite.addTest(makeSuite(AuthorInfoTestCase))
    suite.addTest(makeSuite(ChromeTemplateRenderingTestCase))
    return suite


if __name__ == '__main__':
    unittest.main(defaultTest='test_suite')
