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

import datetime
import json

import tg
from mock import patch

from allura.lib import helpers as h
from alluratest.controller import TestController
from forgeblog import model as BM


class Test(TestController):

    def _post(self, slug='', extra_app_post_params={}, **kw):
        d = {
            'title': 'My Post',
            'text': 'Nothing to see here',
            'labels': '',
            'state': 'published'}
        d.update(kw)
        r = self.app.post('/blog%s/save' % slug, params=d, **extra_app_post_params)
        return r

    def _blog_date(self):
        return datetime.datetime.utcnow().strftime('%Y/%m')

    @patch('forgeblog.model.blog.g.director.create_activity')
    def test_activity(self, create_activity):
        self._post(state='draft')
        assert create_activity.call_count == 0
        slug = '/%s/my-post' % self._blog_date()
        self._post(slug)
        assert create_activity.call_count == 1, create_activity.call_count
        assert create_activity.call_args[0][1] == 'created'
        create_activity.reset_mock()
        self._post(slug, text='new text')
        assert create_activity.call_count == 1
        assert create_activity.call_args[0][1] == 'modified'
        create_activity.reset_mock()
        self._post(slug, title='new title')
        assert create_activity.call_count == 1
        assert create_activity.call_args[0][1] == 'renamed'

    def test_root_index(self):
        self._post()
        d = self._blog_date()
        response = self.app.get('/blog/')
        assert 'Recent posts' in response
        assert 'Nothing to see here' in response
        assert '/blog/%s/my-post/edit' % d in response
        anon_r = self.app.get('/blog/',
                              extra_environ=dict(username='*anonymous'))
        # anonymous user can't see Edit links
        assert 'Nothing to see here' in anon_r
        assert '/blog/%s/my-post/edit' % d not in anon_r

    def test_root_index_draft(self):
        self._post(state='draft')
        d = self._blog_date()
        response = self.app.get('/blog/')
        assert 'Recent posts' in response
        assert 'Nothing to see here' in response
        assert 'Draft' in response
        assert '/blog/%s/my-post/edit' % d in response
        anon_r = self.app.get('/blog/',
                              extra_environ=dict(username='*anonymous'))
        # anonymous user can't see draft posts
        assert 'Nothing to see here' not in anon_r

    def test_root_new_post(self):
        response = self.app.get('/blog/new')
        assert '<option selected value="published">Published</option>' in response
        assert 'Enter your title here' in response

    def test_validation(self):
        r = self._post(title='')
        assert 'You must provide a Title' in r

    def test_root_new_search(self):
        self._post()
        response = self.app.get('/blog/search/?q=see')
        assert 'Search' in response

    def test_paging(self):
        [self._post() for i in range(3)]
        r = self.app.get('/blog/?limit=1&page=0')
        assert 'Newer Entries' not in r
        assert 'Older Entries' in r
        r = self.app.get('/blog/?limit=1&page=1')
        assert 'Newer Entries' in r
        assert 'Older Entries' in r
        r = self.app.get('/blog/?limit=1&page=2')
        assert 'Newer Entries' in r
        assert 'Older Entries' not in r

    def test_discussion_admin(self):
        r = self.app.get('/blog/')
        r = self.app.get('/admin/blog/options', validate_chunk=True)
        assert 'Allow discussion/commenting on posts' in r
        # Turn discussion on
        r = self.app.post('/admin/blog/set_options',
                          params=dict(show_discussion='1'))
        self._post()
        d = self._blog_date()
        r = self.app.get('/blog/%s/my-post/' % d)
        assert '<div class="markdown_edit">' in r
        # Turn discussion off
        r = self.app.post('/admin/blog/set_options')
        r = self.app.get('/blog/%s/my-post/' % d)
        assert '<div class="markdown_edit">' not in r

    def test_post_index(self):
        self._post()
        d = self._blog_date()
        response = self.app.get('/blog/%s/my-post/' % d)
        assert 'Nothing to see here' in response
        assert '/blog/%s/my-post/edit' % d in response
        anon_r = self.app.get('/blog/%s/my-post/' % d,
                              extra_environ=dict(username='*anonymous'))
        # anonymous user can't see Edit links
        assert 'Nothing to see here' in anon_r
        assert '/blog/%s/my-post/edit' % d not in anon_r
        self.app.get('/blog/%s/no-my-post' % d, status=404)

    def test_post_index_draft(self):
        self._post(state='draft')
        d = self._blog_date()
        response = self.app.get('/blog/%s/my-post/' % d)
        assert 'Nothing to see here' in response
        assert 'Draft' in response
        assert '/blog/%s/my-post/edit' % d in response
        anon_r = self.app.get('/blog/%s/my-post/' % d,
                              extra_environ=dict(username='*anonymous'))
        # anonymous user can't get to draft posts
        assert 'Nothing to see here' not in anon_r

    def test_post_edit(self):
        self._post()
        d = self._blog_date()
        response = self.app.get('/blog/%s/my-post/edit' % d)
        assert 'Nothing' in response
        # anon users can't edit
        response = self.app.get('/blog/%s/my-post/edit' % d,
                                extra_environ=dict(username='*anonymous'))
        assert 'Nothing' not in response

    def test_post_get_markdown(self):
        self._post()
        d = self._blog_date()
        response = self.app.get('/blog/%s/my-post/get_markdown' % d)
        assert response.content_type == 'text/plain'
        assert 'Nothing' in response

    def test_post_update_markdown(self):
        self._post()
        d = self._blog_date()
        response = self.app.post(
            '/blog/%s/my-post/update_markdown' % d,
            params={
                'text': '- [x] checkbox'})
        assert response.json['status'] == 'success'
        # anon users can't edit markdown
        response = self.app.post(
            '/blog/%s/my-post/update_markdown' % d,
            params={
                'text': '- [x] checkbox'},
            extra_environ=dict(username='*anonymous'))
        assert response.json['status'] == 'no_permission'

    def test_post_attachments(self):
        # create
        upload = ('attachment', 'nums.txt', b'123412341234')
        upload2 = ('attachment', 'more_nums.txt', b'56789')
        self._post(extra_app_post_params=dict(upload_files=[upload, upload2]))

        # check it is listed
        d = self._blog_date()
        r = self.app.get('/blog/%s/my-post/' % d)
        assert 'nums.txt' in r

        # edit
        slug = '/%s/my-post' % d
        upload = ('attachment', 'letters.txt', b'abcdefghij')
        self._post(slug=slug,
                   extra_app_post_params=dict(upload_files=[upload]))

        # check both are there
        r = self.app.get('/blog/%s/my-post/' % d)
        assert 'nums.txt' in r
        assert 'letters.txt' in r

        # check attachment is accessible
        r = self.app.get('/blog/%s/my-post/attachment/letters.txt' % d)
        assert 'abcdefghij' in r

    def test_post_history(self):
        self._post()
        d = self._blog_date()
        self._post('/%s/my-post' % d)
        self._post('/%s/my-post' % d)
        response = self.app.get('/blog/%s/my-post/history' % d)
        assert 'My Post' in response
        # two revisions are shown
        assert '2 by Test Admin' in response
        assert '1 by Test Admin' in response
        self.app.get('/blog/%s/my-post/?version=1' % d)
        self.app.get('/blog/%s/my-post/?version=foo' % d, status=404)

    def test_post_diff(self):
        self._post()
        d = self._blog_date()
        self._post('/%s/my-post' % d, text='sometext\n<script>alert(1)</script>')
        self.app.post('/blog/%s/my-post/revert' % d, params=dict(version='1'))
        response = self.app.get('/blog/%s/my-post/diff?v2=2&v1=1' % d)
        assert 'My Post' in response
        assert '<del> Nothing to see here </del> <ins> sometext </ins>' in response
        assert '<script>alert' not in response
        assert '<ins> &lt;script&gt;alert' in response

    def test_post_revert(self):
        self._post()
        d = self._blog_date()
        self._post('/%s/my-post' % d, text='sometést'.encode())
        response = self.app.post('/blog/%s/my-post/revert' % d, params=dict(version='1'))
        assert '.' in response.json['location']
        response = self.app.get('/blog/%s/my-post/' % d)
        assert 'Nothing to see here' in response

    def test_invalid_lookup(self):
        r = self.app.get('/blog/favicon.ico', status=404)

    def test_index_bad_url_params(self):
        self.app.get('/blog/?limit=blah&page=2x', status=200)

    def test_post_bad_url_params(self):
        self._post()
        d = self._blog_date()
        self.app.get('/blog/%s/my-post/?limit=blah&page=2x' % d, status=200)

    def test_save_ignores_non_form_fields(self):
        """Mass assignment: extra POST params must not overwrite model fields outside the form"""
        self._post()
        d = self._blog_date()
        post_url = '/blog/%s/my-post/save' % d

        post = BM.BlogPost.query.find().first()
        original_slug = post.slug
        original_app_config_id = post.app_config_id

        # Attempt to inject fields not defined in EditPostForm
        self.app.post(post_url, params={
            'title': 'My Post',
            'text': 'Updated text',
            'labels': '',
            'state': 'published',
            'slug': 'hacked-slug',
            'deleted': 'True',
            'app_config_id': 'deadbeefdeadbeefdeadbeef',
        })

        post = BM.BlogPost.query.find({'slug': original_slug}).first()
        assert post is not None, "post should still be findable by its original slug"
        assert post.text == 'Updated text'  # standard fields should be updated
        assert post.slug == original_slug
        assert post.deleted is False
        assert post.app_config_id == original_app_config_id

    def test_rate_limit_submit(self):
        with h.push_config(tg.config, **{'forgeblog.rate_limits': '{"3600": 0}'}):
            r = self._post()
            wf = json.loads(self.webflash(r))
            assert wf['status'] == 'error'
            assert wf['message'] == 'Create/edit rate limit exceeded. Please try again later.'

    def test_rate_limit_form(self):
        with h.push_config(tg.config, **{'forgeblog.rate_limits': '{"3600": 0}'}):
            r = self.app.get('/blog/new')
            wf = json.loads(self.webflash(r))
            assert wf['status'] == 'error'
            assert wf['message'] == 'Create/edit rate limit exceeded. Please try again later.'

    def test_admin_external_feed_invalid(self):
        r = self.app.get('/blog/')
        r = self.app.get('/admin/blog/exfeed')
        form = r.forms[0]
        form['new_exfeed'].value = 'asdfasdf'
        r = form.submit()
        assert 'Invalid' in self.webflash(r)

    def test_admin_external_feed_ok(self):
        # sidebar menu doesn't expose link to this, unless "forgeblog.exfeed" config is true, but can use form anyway
        r = self.app.get('/blog/')
        r = self.app.get('/admin/blog/exfeed')
        form = r.forms[0]
        form['new_exfeed'].value = 'https://example.com/feed.rss'
        r = form.submit()
        assert 'External feeds updated' in self.webflash(r)

        r = self.app.get('/admin/blog/exfeed')
        r.mustcontain('https://example.com/feed.rss')
