from __future__ import absolute_import, division, print_function

import datetime
import email
from glob import glob

try:  # pragma nocover
    import unittest.mock as mock
except ImportError:  # pragma nocover
    # py2
    import mock  # type: ignore

import os
import os.path
import re
import subprocess
from time import sleep

import feedparser
import html2text
import pytest
from pkg_resources import parse_version

import feed2exec
import feed2exec.plugins as plugins
import feed2exec.plugins.archive as archive_plugin
import feed2exec.plugins.maildir as maildir_plugin
import feed2exec.plugins.transmission as transmission_plugin
import feed2exec.utils as utils
from feed2exec.controller import Feed
from feed2exec.tests.test_feeds import test_params, test_sample


def test_maildir(tmpdir, feed_manager, static_boundary):
    global LOCK
    LOCK = mock.MagicMock()

    feed = {'name': 'INBOX', "mailbox": str(tmpdir.join('Mail'))}
    item = {'summary': 'body',
            'title': 'subject',
            'link': 'http://example.com/',
            'published_parsed': datetime.datetime.now()}

    f = maildir_plugin.output(to_addr='nobody@example.com', session=feed_manager.session,
                              feed=feed, item=item, lock=LOCK)
    message = tmpdir.join('Mail', 'inbox', 'new', f.key)
    assert message.check()
    raw = message.read()
    assert 'base64' not in raw
    assert '==' not in raw

    # subject header hijack protection
    item['title'] = 'subject\nX-Header-Hijack: true'
    with pytest.raises(email.errors.HeaderParseError):
        maildir_plugin.output(to_addr='nobody@example.com', session=feed_manager.session,
                              feed=feed, item=item, lock=LOCK)
    sample = Feed('maildir test',
                  {'url': test_sample['url'],
                   'email': 'from@example.com',
                   'output': 'feed2exec.plugins.maildir',
                   'mailbox': str(tmpdir.join('Mail')),
                   'args': 'to@example.com'})
    body = feed_manager.session.get(sample['url']).content
    data = feed_manager.dispatch(sample, sample.parse(body), lock=LOCK)
    folder = utils.slug(sample['name'])
    for message in tmpdir.join('Mail', folder, 'new').visit():
        expected = '''Content-Type: multipart/alternative; boundary="===============testboundary=="
MIME-Version: 1.0
Date: Sun, 06 Sep 2009 16:20:00 -0000
To: to@example.com
From: test author <from@example.com>
Subject: Example entry
Message-ID: <7bd204c6-1655-4c27-aeee-53f933c5395f>
User-Agent: feed2exec (%s)
Precedence: list
Auto-Submitted: auto-generated
Archived-At: http://www.example.com/blog/post/1

--===============testboundary==
Content-Type: text/html; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 8bit

This is the  body, which should show instead of the above
--===============testboundary==
Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 8bit

http://www.example.com/blog/post/1

This is the body, which should show instead of the above


--===============testboundary==--
'''  # noqa
        assert (expected % feed2exec.__version__) == message.read()
    # test if folder setting works
    sample['folder'] = 'folder-test'
    body = feed_manager.session.get(sample['url']).content
    data = feed_manager.dispatch(sample, sample.parse(body), lock=LOCK)
    for item in data['entries']:
        f = plugins.output(sample, item, lock=LOCK, session=feed_manager.session)
        message = tmpdir.join('Mail', 'folder-test', 'new', f.key)
        assert message.check()

    tmpdir.join('Mail', 'inbox').remove()
    feed['catchup'] = True
    f = maildir_plugin.output(to_addr='nobody@example.com', session=feed_manager.session,
                              feed=feed, item=item, lock=LOCK)
    message = tmpdir.join('Mail', 'inbox', 'new', f.key)
    assert not message.check()


@pytest.mark.xfail(
    condition=parse_version(feedparser.__version__) < parse_version('5.2.1'),
    reason="older feedparser version do not sort <img> tags, install feedparser 5.2.1 or later",
)
@pytest.mark.xfail(
    condition=html2text.__version__ < (2020, 1, 16),
    reason="older html2text output varies, install version 2020.1.16 or later",
)
def test_email(tmpdir, feed_manager, static_boundary):
    global LOCK
    LOCK = mock.MagicMock()

    testdir = utils.find_test_file('.')
    for path in glob(os.path.join(testdir, '*.xml')):
        feed = Feed(os.path.basename(path)[:-4],
                    {'url': 'file://' + path,
                     'output': 'feed2exec.plugins.mbox',
                     'mailbox': str(tmpdir.join('Mail')),
                     'email': 'from@example.com',
                     'args': 'to@example.com',
                     'filter': 'feed2exec.plugins.droptitle',
                     'filter_args': 'Trump'})
        body = feed_manager.session.get(feed['url']).content
        feed_manager.dispatch(feed, feed.parse(body), lock=LOCK)
        folder = utils.slug(feed['name']) + '.mbx'
        ua_pattern = re.compile('User-Agent: .*$', flags=re.MULTILINE)
        actual_path = tmpdir.join('Mail', folder)
        actual = ua_pattern.sub('', actual_path.read())
        assert actual

        mbox_path = path[:-3] + 'mbx'
        try:
            with open(mbox_path) as expected:
                expect = ua_pattern.sub('', expected.read())
        except FileNotFoundError:
            # ignore missing mailbox samples
            pass
        else:
            assert expect == actual, "%s different from expected %s" % (actual_path, mbox_path)
    assert path


def test_echo(capfd):
    item = feedparser.FeedParserDict({'title': 'bar'})
    e = plugins.output(feed={'output': 'feed2exec.plugins.echo',
                             'args': 'foo {item[title]}'},
                       item=item)
    assert e.called
    out, err = capfd.readouterr()
    assert "foo bar\n" == out


def test_error():
    # shouldn't raise
    plugins.output(feed={'output': 'feed2exec.plugins.error',
                         'args': ''},
                   item={})


def test_exec(capfd):
    e = plugins.output(feed={'output': 'feed2exec.plugins.exec',
                             'args': 'seq 1'},
                       item={})
    out, err = capfd.readouterr()
    assert "1\n" == out
    assert 0 == e
    e = plugins.output(feed={'output': 'feed2exec.plugins.exec',
                             'args': 'seq 1', 'catchup': 'True'},
                       item={})
    out, err = capfd.readouterr()
    assert "" == out
    assert e


def test_filter():
    item = {'title': 'test'}
    expected = item.copy()
    p = plugins.filter(feed={'filter': 'feed2exec.plugins.echo'}, item=item)
    assert expected == item
    assert p
    assert p.called is not None
    item = {'title': 'test'}
    plugins.filter(feed={'filter': 'feed2exec.plugins.null'}, item=item)
    assert expected != item
    assert p.called is not None


def test_matchtitleregex_example_matched():
    item = {'title': 'spam ham'}
    feed = {
        'filter': 'feed2exec.plugins.matchtitleregex',
        'filter_args': '^s.* ham$',
    }
    plugins.filter(feed=feed, item=item)
    assert {'title': 'spam ham', 'skip': False} == item


def test_matchtitleregex_example_not_matched():
    item = {'title': 'spam ham'}
    feed = {
        'filter': 'feed2exec.plugins.matchtitleregex',
        'filter_args': 'egg',
    }
    plugins.filter(feed=feed, item=item)
    assert {'title': 'spam ham', 'skip': True} == item


def test_droptitleregex_example_matched():
    item = {'title': 'spam ham'}
    feed = {
        'filter': 'feed2exec.plugins.droptitleregex',
        'filter_args': '^s.* ham$',
    }
    plugins.filter(feed=feed, item=item)
    assert {'title': 'spam ham', 'skip': True} == item


def test_droptitleregex_example_not_matched():
    item = {'title': 'spam ham'}
    feed = {
        'filter': 'feed2exec.plugins.droptitleregex',
        'filter_args': 'egg',
    }
    plugins.filter(feed=feed, item=item)
    assert {'title': 'spam ham', 'skip': False} == item


@pytest.fixture(params=['page', 'full'])
def wayback_feed(request):
    config = {'output': 'feed2exec.plugins.wayback', 'args': request.param}
    return Feed('wayback test', config)


def test_wayback_archive(feed_manager, logging_handler, wayback_feed):
    full = 'args' not in wayback_feed or 'full' in wayback_feed['args']
    item = feedparser.FeedParserDict({'link': 'http://archive.org/'})
    e = plugins.output(feed=wayback_feed, item=item, session=feed_manager.session)
    assert not e
    for record in logging_handler.buffer:
        if 'wayback machine' in record.getMessage():
            break
    else:  # sanity check
        raise AttributeError('no wayback logs generated?')  # pragma: nocover
    assert 'WARNING' == record.levelname
    if full:
        assert 'wayback machine failed to save URL %s: %s' == record.msg
        error = record.args[1]
    else:
        assert 'wayback machine failed to save URL %s, status %d %s: %s' == record.msg
        assert 523 == record.args[1]
        error = record.args[3]
    assert 'Cannot save Internet Archive URLs! http://archive.org/.' == error


def test_wayback_invalid_example(feed_manager, logging_handler, wayback_feed):
    full = 'args' not in wayback_feed or 'full' in wayback_feed['args']
    item = feedparser.FeedParserDict({'link': 'http://invalid.example.com/'})
    e = plugins.output(feed=wayback_feed, item=item, session=feed_manager.session)
    assert not e
    for record in logging_handler.buffer:
        if 'wayback machine' in record.getMessage():
            break
    else:  # sanity check
        raise AttributeError('no wayback logs generated?')  # pragma: nocover
    assert 'WARNING' == record.levelname
    if full:
        assert 'wayback machine failed to save URL %s: %s' == record.msg
        error = record.args[1]
    else:
        assert 'wayback machine failed to save URL %s, status %d %s: %s' == record.msg
        assert 523 == record.args[1]
        error = record.args[3]
    assert 'Cannot resolve host invalid.example.com.' == error


def test_wayback_example_invalid(feed_manager, logging_handler, wayback_feed):
    full = 'args' not in wayback_feed or 'full' in wayback_feed['args']
    item = feedparser.FeedParserDict({'link': 'http://example.invalid/'})
    e = plugins.output(feed=wayback_feed, item=item, session=feed_manager.session)
    assert not e
    for record in logging_handler.buffer:
        if 'wayback machine' in record.getMessage():
            break
    else:  # sanity check
        raise AttributeError('no wayback logs generated?')  # pragma: nocover
    assert 'WARNING' == record.levelname
    if full:
        assert 'wayback machine failed to save URL %s: %s' == record.msg
        error = record.args[1]
    else:
        assert 'wayback machine failed to save URL %s, status %d %s: %s' == record.msg
        assert 523 == record.args[1]
        error = record.args[3]
    assert 'http://example.invalid/ URL syntax is not valid.' == error


def test_wayback_example_working(feed_manager, logging_handler, wayback_feed):
    full = 'args' not in wayback_feed or 'full' in wayback_feed['args']
    if full:
        link = 'http://example.com/'
    else:
        link = 'http://www.example.com/'
    item = feedparser.FeedParserDict({'link': link})
    e = plugins.output(feed=wayback_feed, item=item, session=feed_manager.session)
    assert e
    for record in logging_handler.buffer:
        if 'wayback machine' in record.getMessage():
            break
    else:  # sanity check
        raise AttributeError('no wayback logs generated?')  # pragma: nocover
    assert 'INFO' == record.levelname
    if full:
        assert 'URL %s with resources probably saved to wayback machine' == record.msg
    else:
        assert 'URL %s saved without resources to wayback machine: %s' == record.msg
        assert record.args[1].startswith('https://web.archive.org/web/')


def test_wayback_example_too_fast(feed_manager_recorder, logging_handler, wayback_feed):
    recorder, feed_manager = feed_manager_recorder
    # SPN1 does not error when archiving the same domain too fast
    # so this test is only applicable to the SPN2 API.
    full = 'args' not in wayback_feed or 'full' in wayback_feed['args']
    if not full:
        pytest.skip()
    # Use example.org so we do not get false failures
    # due to other tests, which all use example.com
    item = feedparser.FeedParserDict({'link': 'http://example.org/'})
    e = plugins.output(feed=wayback_feed, item=item, session=feed_manager.session)
    assert e
    for record in logging_handler.buffer:
        if 'wayback machine' in record.getMessage():
            break
    else:  # sanity check
        raise AttributeError('no wayback logs generated?')  # pragma: nocover
    assert 'INFO' == record.levelname
    assert 'URL %s with resources probably saved to wayback machine' == record.msg

    # Wait for the wayback machine to register the first request for the URL
    # otherwise the second archiving request will succeed instead of failing.
    # Don't bother waiting when we aren't really interacting with archive.org.
    if recorder.current_cassette.is_recording():
        sleep(30)

    # Drop the messages from the first archiving request
    logging_handler.buffer.clear()

    # Now test that second archiving of same URL fails with "too fast" msg
    e = plugins.output(feed=wayback_feed, item=item, session=feed_manager.session)
    assert not e
    for record in logging_handler.buffer:
        if 'wayback machine' in record.getMessage():
            break
    else:  # sanity check
        raise AttributeError('no wayback logs generated?')  # pragma: nocover
    assert 'WARNING' == record.levelname
    assert 'wayback machine failed to save URL %s: %s' == record.msg
    assert re.match(
        r'The same snapshot had been made .* ago\. We only allow new captures of the same URL every \d+ minutes\.',
        record.args[1]
    )


def test_wayback_example_404(feed_manager, logging_handler, wayback_feed):
    # SPN2 does not return errors for HTTP error codes
    # even if the error page archiving is turned off.
    full = 'args' not in wayback_feed or 'full' in wayback_feed['args']
    if full:
        pytest.skip()
    item = feedparser.FeedParserDict({'link': 'http://example.com/404'})
    e = plugins.output(feed=wayback_feed, item=item, session=feed_manager.session)
    assert not e
    for record in logging_handler.buffer:
        if 'wayback machine' in record.getMessage():
            break
    else:  # sanity check
        raise AttributeError('no wayback logs generated?')  # pragma: nocover
    assert 'WARNING' == record.levelname
    assert 'wayback machine failed to save URL %s, status %d %s: %s' == record.msg
    assert 523 == record.args[1]
    assert 'The server cannot find the requested resource http://example.com/404 (HTTP status=404).' == record.args[3]


def test_wayback_catchup(feed_manager, logging_handler, wayback_feed):
    full = 'args' not in wayback_feed or 'full' in wayback_feed['args']
    if full:
        links = [
            'https://anarc.at/wikiicons/diff.png',
            'http://example.net/',
        ]
    else:
        links = [
            'https://anarc.at/wikiicons/email.png',
            'http://www.example.net/',
        ]
    item = feedparser.FeedParserDict({'link': links[0]})
    e = plugins.output(feed=wayback_feed, item=item, session=feed_manager.session)
    assert e

    called = False

    def fake(*args, **kwargs):
        nonlocal called
        called = True
        return mock.MagicMock()

    feed_manager.session.get = fake
    feed_manager.session.post = fake
    item = feedparser.FeedParserDict({'link': links[1]})
    e = plugins.output(feed=wayback_feed, item=item, session=feed_manager.session)
    assert called
    wayback_feed['catchup'] = True
    called = False
    e = plugins.output(feed=wayback_feed, item=item, session=feed_manager.session)
    assert not called


def test_transmission(monkeypatch):
    capture = []

    def fake_call(*args, **kwargs):
        capture.append(*args)

    monkeypatch.setattr(subprocess, 'check_call', fake_call)
    item = {'summary': 'body',
            'title': 'Evil/../../../etc/password',
            'link': 'http://example.com/',
            'published_parsed': datetime.datetime.now()}
    transmission_plugin.output(hostname='example.com',
                               feed=test_sample, item=item)
    assert [['transmission-remote', 'example.com',
             '-a', 'http://example.com/']] == capture
    capture = []
    transmission_plugin.output(hostname='example.com',
                               feed=test_params, item=item)
    assert [['transmission-remote', 'example.com',
             '-a', 'http://example.com/',
             '-w', 'test-folder/Evil.etc.password']] == capture
    test_params['catchup'] = True
    capture = []
    transmission_plugin.output(hostname='example.com',
                               feed=test_params, item=item)
    assert [] == capture


def test_archive(tmpdir, feed_manager):
    dest = tmpdir.join('archive')
    feed = Feed('test archive', test_sample)
    item = feedparser.FeedParserDict({'link': 'http://example.com/',
                                      'title': 'example site'})
    assert archive_plugin.output(str(dest), feed=feed, item=item, session=feed_manager.session)
    assert dest.join('example-site').check()
    dest.remove()
    item = feedparser.FeedParserDict({'link': 'http://example.com/404',
                                      'title': 'example site'})
    assert not archive_plugin.output(str(dest), feed=feed, item=item, session=feed_manager.session)
    assert not dest.join('example-site').check()
    # no link
    item = feedparser.FeedParserDict({'title': 'example site'})
    assert archive_plugin.output(str(dest), feed=feed, item=item, session=feed_manager.session)
    assert not dest.join('example-site').check()
    feed['catchup'] = True
    item = feedparser.FeedParserDict({'link': 'http://example.com/',
                                      'title': 'example site'})
    assert archive_plugin.output(str(dest), feed=feed, item=item, session=feed_manager.session)
    assert not dest.join('example-site').check()
