#       Licensed to the Apache Software Foundation (ASF) under one
#       or more contributor license agreements.  See the NOTICE file
#       distributed with this work for additional information
#       regarding copyright ownership.  The ASF licenses this file
#       to you under the Apache License, Version 2.0 (the
#       "License"); you may not use this file except in compliance
#       with the License.  You may obtain a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#       Unless required by applicable law or agreed to in writing,
#       software distributed under the License is distributed on an
#       "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
#       KIND, either express or implied.  See the License for the
#       specific language governing permissions and limitations
#       under the License.

from tg import tmpl_context as c
import mock
from ming.base import Object
import pytest
from formencode import validators as fev
from textwrap import dedent
import tempfile
import os.path

from alluratest.controller import setup_unit_test
from allura import app
from allura.lib.app_globals import Icon
from allura.lib import mail_util


class TestApp:

    def setup_method(self):
        setup_unit_test()
        c.user._id = None
        c.project = mock.Mock()
        c.project.name = 'Test Project'
        c.project.shortname = 'tp'
        c.project._id = 'testproject/'
        c.project.url = lambda: '/testproject/'
        app_config = mock.Mock()
        app_config._id = None
        app_config.project_id = 'testproject/'
        app_config.tool_name = 'tool'
        app_config.options = Object(mount_point='foo')
        c.app = mock.Mock()
        c.app.config = app_config
        c.app.config.script_name = lambda: '/testproject/test_application/'
        c.app.config.url = lambda: 'http://testproject/test_application/'
        c.app.url = c.app.config.url()
        c.app.__version__ = '0.0'

    def test_config_options(self):
        options = [
            app.ConfigOption('test1', str, 'MyTestValue'),
            app.ConfigOption('test2', str, lambda: 'MyTestValue')]
        assert options[0].default == 'MyTestValue'
        assert options[1].default == 'MyTestValue'

    def test_config_options_render_attrs(self):
        opt = app.ConfigOption('test1', str, None, extra_attrs={'type': 'url'})
        assert opt.render_attrs() == 'type="url"'

    def test_config_option_without_validator(self):
        opt = app.ConfigOption('test1', str, None)
        assert opt.validate(None) is None
        assert opt.validate('') == ''
        assert opt.validate('val') == 'val'

    def test_config_option_with_validator(self):
        v = fev.NotEmpty()
        opt = app.ConfigOption('test1', str, None, validator=v)
        assert opt.validate('val') == 'val'
        with pytest.raises(fev.Invalid):
            opt.validate(None)
        with pytest.raises(fev.Invalid):
            opt.validate('')

    def test_options_on_install_default(self):
        a = app.Application(c.project, c.app.config)
        assert a.options_on_install() == []

    def test_options_on_install(self):
        opts = [app.ConfigOption('url', str, None),
                app.ConfigOption('private', bool, None)]

        class TestApp(app.Application):
            config_options = app.Application.config_options + opts + [
                app.ConfigOption('not_on_install', str, None),
            ]
            config_on_install = ['url', 'private']

        a = TestApp(c.project, c.app.config)
        assert a.options_on_install() == opts

    def test_main_menu(self):
        class TestApp(app.Application):
            @property
            def sitemap(self):
                children = [app.SitemapEntry('New', 'new', ui_icon=Icon('some-icon')),
                            app.SitemapEntry('Recent', 'recent'),
                            ]
                return [app.SitemapEntry('My Tool', '.')[children]]

        a = TestApp(c.project, c.app.config)
        main_menu = a.main_menu()
        assert len(main_menu) == 1
        assert main_menu[0].children == []  # default main_menu implementation should drop the children from sitemap()

    def test_sitemap(self):
        sm = app.SitemapEntry('test', '')[
            app.SitemapEntry('a', 'a/'),
            app.SitemapEntry('b', 'b/')]
        sm[app.SitemapEntry(lambda app: app.config.script_name(), 'c/')]
        bound_sm = sm.bind_app(c.app)
        assert bound_sm.url == 'http://testproject/test_application/', bound_sm.url
        assert bound_sm.children[
            -1].label == '/testproject/test_application/', bound_sm.children[-1].label
        assert len(sm.children) == 3
        sm.extend([app.SitemapEntry('a', 'a/')[
            app.SitemapEntry('d', 'd/')]])
        assert len(sm.children) == 3

    @mock.patch('allura.app.Application.PostClass.query.get')
    def test_handle_artifact_unicode(self, qg):
        """
        Tests that app.handle_artifact_message can accept utf strings
        """
        ticket = mock.MagicMock()
        ticket.get_discussion_thread.return_value = (mock.MagicMock(), mock.MagicMock())
        post = mock.MagicMock()
        qg.return_value = post

        a = app.Application(c.project, c.app.config)

        msg = dict(payload='foo ƒ†©¥˙¨ˆ'.encode(), message_id=1, headers={})
        a.handle_artifact_message(ticket, msg)
        assert post.attach.call_args[0][1].getvalue() == 'foo ƒ†©¥˙¨ˆ'.encode()

        msg = dict(payload=b'foo', message_id=1, headers={})
        a.handle_artifact_message(ticket, msg)
        assert post.attach.call_args[0][1].getvalue() == b'foo'

        msg = dict(payload="\x94my quote\x94".encode(), message_id=1, headers={})
        a.handle_artifact_message(ticket, msg)
        assert post.attach.call_args[0][1].getvalue() == '\x94my quote\x94'.encode()

        # assert against prod example
        msg_raw = dedent("""\
            Message-Id: <1502352031.3216858.1068961568.19EF48C6@webmail.messagingengine.com>
            From: foo <foo@bar.com>
            To: "[forge:site-support]" <15391@site-support.forge.p.re.sf.net>
            MIME-Version: 1.0
            Content-Transfer-Encoding: 7bit
            Content-Type: multipart/alternative; boundary="_----------=_150235203132168580"
            Date: Thu, 10 Aug 2017 10:00:31 +0200
            Subject: Re: [forge:site-support] #15391 Unable to join (my own) mailing list
            This is a multi-part message in MIME format.
            --_----------=_150235203132168580
            Content-Transfer-Encoding: quoted-printable
            Content-Type: text/plain; charset="utf-8"
            Hi
            --_----------=_150235203132168580
            Content-Transfer-Encoding: quoted-printable
            Content-Type: text/html; charset="utf-8"
            <!DOCTYPE html>
            <html><body>Hi</body></html>
            --_----------=_150235203132168580--
        """)
        msg = mail_util.parse_message(msg_raw)
        for p in [p for p in msg['parts'] if p['payload'] is not None]:
            # filter here mimics logic in `route_email`
            a.handle_artifact_message(ticket, p)

    def test_save_attachments_path_traversal_protection(self):
        a = app.Application(c.project, c.app.config)
        export_dir = tempfile.mkdtemp()

        class FakeAttachment:
            def __init__(self, filename, data):
                self.filename = filename
                self._data = data

            def rfile(self):
                from io import BytesIO
                return BytesIO(self._data)

        # Test with a malicious filename containing path traversal
        malicious_name = '../../allura-test-case/foobar'
        attachments = [FakeAttachment(malicious_name, b'evil data')]
        a.save_attachments(export_dir, attachments)

        # The file should be written as a basename, not traverse directories
        expected_file = os.path.join(export_dir, 'foobar')
        assert os.path.exists(expected_file)

        # Verify the traversal path was NOT created
        traversal_path = os.path.join(export_dir, '..', '..', 'allura-test-case', 'foobar')
        assert not os.path.exists(traversal_path)

        # Verify file contents
        with open(expected_file, 'rb') as f:
            assert f.read() == b'evil data'
