# -*- coding: utf-8 -*-
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
#    this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
#    this list of conditions and the following disclaimer in the documentation
#    and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

import json
import logging
import os
from datetime import datetime
from datetime import timezone
from unittest.mock import Mock, patch

import pytest
import requests

from apprise import Apprise
from apprise import NotifyType
from apprise import AppriseAttachment
from apprise.plugins.NotifyTwitter import NotifyTwitter
from helpers import AppriseURLTester

# Disable logging for a cleaner testing output
logging.disable(logging.CRITICAL)

# Attachment Directory
TEST_VAR_DIR = os.path.join(os.path.dirname(__file__), 'var')

TWITTER_SCREEN_NAME = 'apprise'


# Our Testing URLs
apprise_url_tests = (
    ##################################
    # NotifyTwitter
    ##################################
    ('twitter://', {
        # Missing Consumer API Key
        'instance': TypeError,
    }),
    ('twitter://:@/', {
        'instance': TypeError,
    }),
    ('twitter://consumer_key', {
        # Missing Keys
        'instance': TypeError,
    }),
    ('twitter://consumer_key/consumer_secret/', {
        # Missing Keys
        'instance': TypeError,
    }),
    ('twitter://consumer_key/consumer_secret/atoken1/', {
        # Missing Access Secret
        'instance': TypeError,
    }),
    ('twitter://consumer_key/consumer_secret/atoken2/access_secret', {
        # No user mean's we message ourselves
        'instance': NotifyTwitter,
        # Expected notify() response False (because we won't be able
        # to detect our user)
        'notify_response': False,

        # Our expected url(privacy=True) startswith() response:
        'privacy_url': 'x://c...y/****/a...2/****',
    }),
    ('twitter://consumer_key/consumer_secret/atoken3/access_secret'
        '?cache=no', {
            # No user mean's we message ourselves
            'instance': NotifyTwitter,
            # However we'll be okay if we return a proper response
            'requests_response_text': {
                'id': 12345,
                'screen_name': 'test',
                # For attachment handling
                'media_id': 123,
            },
        }),
    ('twitter://consumer_key/consumer_secret/atoken4/access_secret', {
        # No user mean's we message ourselves
        'instance': NotifyTwitter,
        # However we'll be okay if we return a proper response
        'requests_response_text': {
            'id': 12345,
            'screen_name': 'test',
            # For attachment handling
            'media_id': 123,
        },
    }),
    # A duplicate of the entry above, this will cause cache to be referenced
    ('twitter://consumer_key/consumer_secret/atoken5/access_secret', {
        # No user mean's we message ourselves
        'instance': NotifyTwitter,
        # However we'll be okay if we return a proper response
        'requests_response_text': {
            'id': 12345,
            'screen_name': 'test',
            # For attachment handling
            'media_id': 123,
        },
    }),
    # handle cases where the screen_name is missing from the response causing
    # an exception during parsing
    ('twitter://consumer_key/consumer_secret2/atoken6/access_secret', {
        # No user mean's we message ourselves
        'instance': NotifyTwitter,
        # However we'll be okay if we return a proper response
        'requests_response_text': {
            'id': 12345,
            # For attachment handling
            'media_id': 123,
        },
        # due to a mangled response_text we'll fail
        'notify_response': False,
    }),
    ('twitter://user@consumer_key/csecret2/atoken7/access_secret/-/%/', {
        # One Invalid User
        'instance': NotifyTwitter,
        # Expected notify() response False (because we won't be able
        # to detect our user)
        'notify_response': False,
    }),
    ('twitter://user@consumer_key/csecret/atoken8/access_secret'
        '?cache=No&batch=No', {
            # No Cache & No Batch
            'instance': NotifyTwitter,
            'requests_response_text': [{
                'id': 12345,
                'screen_name': 'user'
            }],
        }),
    ('twitter://user@consumer_key/csecret/atoken9/access_secret', {
        # We're good!
        'instance': NotifyTwitter,
        'requests_response_text': [{
            'id': 12345,
            'screen_name': 'user'
        }],
    }),
    ('twitter://user@consumer_key/csecret/atoken11/access_secret', {
        # We're identifying the same user we already sent to
        'instance': NotifyTwitter,
        'notify_response': False,
    }),
    ('tweet://ckey/csecret/atoken12/access_secret', {
        # A Public Tweet
        'instance': NotifyTwitter,
    }),
    ('twitter://user@ckey/csecret/atoken13/access_secret?mode=invalid', {
        # An invalid mode
        'instance': TypeError,
    }),
    ('twitter://usera@consumer_key/consumer_secret/atoken14/'
        'access_secret/user/?to=userb', {
            # We're good!
            'instance': NotifyTwitter,
            'requests_response_text': [{
                'id': 12345,
                'screen_name': 'usera'
            }, {
                'id': 12346,
                'screen_name': 'userb'
            }, {
                # A garbage entry we can test exception handling on
                'id': 123,
            }],
        }),
    ('twitter://ckey/csecret/atoken15/access_secret', {
        'instance': NotifyTwitter,
        # throw a bizzare code forcing us to fail to look it up
        'response': False,
        'requests_response_code': 999,
    }),
    ('twitter://ckey/csecret/atoken16/access_secret', {
        'instance': NotifyTwitter,
        # Throws a series of connection and transfer exceptions when this flag
        # is set and tests that we gracfully handle them
        'test_requests_exceptions': True,
    }),
    ('twitter://ckey/csecret/atoken17/access_secret?mode=tweet', {
        'instance': NotifyTwitter,
        # Throws a series of connection and transfer exceptions when this flag
        # is set and tests that we gracfully handle them
        'test_requests_exceptions': True,
    }),
)


def good_response(data):
    """
    Prepare a good response.
    """
    response = Mock()
    response.content = json.dumps(data)
    response.status_code = requests.codes.ok
    return response


def bad_response(data):
    """
    Prepare a bad response.
    """
    response = Mock()
    response.content = json.dumps(data)
    response.status_code = requests.codes.internal_server_error
    return response


@pytest.fixture
def twitter_url():
    ckey = 'ckey'
    csecret = 'csecret'
    akey = 'akey'
    asecret = 'asecret'
    url = 'twitter://{}/{}/{}/{}'.format(ckey, csecret, akey, asecret)
    return url


@pytest.fixture
def good_message_response():
    """
    Prepare a good tweet response.
    """
    response = good_response({
        'screen_name': TWITTER_SCREEN_NAME,
        'id': 9876,
    })
    return response


@pytest.fixture
def bad_message_response():
    """
    Prepare a bad message response.
    """
    response = bad_response({
        "errors": [
            {
                "code": 999,
                "message": "Something failed",
            }]})
    return response


@pytest.fixture
def good_media_response():
    """
    Prepare a good media response.
    """
    response = Mock()
    response.content = json.dumps({
        "media_id": 710511363345354753,
        "media_id_string": "710511363345354753",
        "media_key": "3_710511363345354753",
        "size": 11065,
        "expires_after_secs": 86400,
        "image": {
            "image_type": "image/jpeg",
            "w": 800,
            "h": 320
        }
    })
    response.status_code = requests.codes.ok
    return response


@pytest.fixture
def bad_media_response():
    """
    Prepare a bad media response.
    """
    response = bad_response({
        "errors": [
            {
                "code": 93,
                "message": "This application is not allowed to access or "
                "delete your direct messages.",
            }]})
    return response


@pytest.fixture(autouse=True)
def ensure_get_verify_credentials_is_mocked(mocker, good_message_response):
    """
    Make sure requests to https://api.twitter.com/1.1/account/verify_credentials.json
    do not escape the test harness, for all test case functions.
    """  # noqa:E501
    mock_get = mocker.patch("requests.get")
    mock_get.return_value = good_message_response


def test_plugin_twitter_urls():
    """
    NotifyTwitter() Apprise URLs
    """

    # Run our general tests
    AppriseURLTester(tests=apprise_url_tests).run_all()


def test_plugin_twitter_general(mocker):
    """
    NotifyTwitter() General Tests
    """

    mock_get = mocker.patch("requests.get")
    mock_post = mocker.patch("requests.post")

    ckey = 'ckey'
    csecret = 'csecret'
    akey = 'akey'
    asecret = 'asecret'

    response_obj = [{
        'screen_name': TWITTER_SCREEN_NAME,
        'id': 9876,
    }]

    # Epoch time:
    epoch = datetime.fromtimestamp(0, timezone.utc)

    request = Mock()
    request.content = json.dumps(response_obj)
    request.status_code = requests.codes.ok
    request.headers = {
        'x-rate-limit-reset': (
            datetime.now(timezone.utc) - epoch).total_seconds(),
        'x-rate-limit-remaining': 1,
    }

    # Prepare Mock
    mock_get.return_value = request
    mock_post.return_value = request

    # Variation Initializations
    obj = NotifyTwitter(
        ckey=ckey,
        csecret=csecret,
        akey=akey,
        asecret=asecret,
        targets=TWITTER_SCREEN_NAME)

    assert isinstance(obj, NotifyTwitter) is True
    assert isinstance(obj.url(), str) is True

    # apprise room was found
    assert obj.send(body="test") is True

    # Change our status code and try again
    request.status_code = 403
    assert obj.send(body="test") is False
    assert obj.ratelimit_remaining == 1

    # Return the status
    request.status_code = requests.codes.ok
    # Force a reset
    request.headers['x-rate-limit-remaining'] = 0
    # behind the scenes, it should cause us to update our rate limit
    assert obj.send(body="test") is True
    assert obj.ratelimit_remaining == 0

    # This should cause us to block
    request.headers['x-rate-limit-remaining'] = 10
    assert obj.send(body="test") is True
    assert obj.ratelimit_remaining == 10

    # Handle cases where we simply couldn't get this field
    del request.headers['x-rate-limit-remaining']
    assert obj.send(body="test") is True
    # It remains set to the last value
    assert obj.ratelimit_remaining == 10

    # Reset our variable back to 1
    request.headers['x-rate-limit-remaining'] = 1

    # Handle cases where our epoch time is wrong
    del request.headers['x-rate-limit-reset']
    assert obj.send(body="test") is True

    # Return our object, but place it in the future forcing us to block
    request.headers['x-rate-limit-reset'] = \
        (datetime.now(timezone.utc) - epoch).total_seconds() + 1
    request.headers['x-rate-limit-remaining'] = 0
    obj.ratelimit_remaining = 0
    assert obj.send(body="test") is True

    # Return our object, but place it in the future forcing us to block
    request.headers['x-rate-limit-reset'] = \
        (datetime.now(timezone.utc) - epoch).total_seconds() - 1
    request.headers['x-rate-limit-remaining'] = 0
    obj.ratelimit_remaining = 0
    assert obj.send(body="test") is True

    # Return our limits to always work
    request.headers['x-rate-limit-reset'] = \
        (datetime.now(timezone.utc) - epoch).total_seconds()
    request.headers['x-rate-limit-remaining'] = 1
    obj.ratelimit_remaining = 1

    # Alter pending targets
    obj.targets.append('usera')
    request.content = json.dumps(response_obj)
    response_obj = [{
        'screen_name': 'usera',
        'id': 1234,
    }]

    assert obj.send(body="test") is True

    # Flush our cache forcing it is re-creating
    NotifyTwitter._user_cache = {}
    assert obj.send(body="test") is True

    # Cause content response to be None
    request.content = None
    assert obj.send(body="test") is True

    # Invalid JSON
    request.content = '{'
    assert obj.send(body="test") is True

    # Return it to a parseable string
    request.content = '{}'

    results = NotifyTwitter.parse_url(
        'twitter://{}/{}/{}/{}?to={}'.format(
            ckey, csecret, akey, asecret, TWITTER_SCREEN_NAME))
    assert isinstance(results, dict) is True
    assert TWITTER_SCREEN_NAME in results['targets']

    # cause a json parsing issue now
    response_obj = None
    assert obj.send(body="test") is True

    response_obj = '{'
    assert obj.send(body="test") is True

    # Set ourselves up to handle whoami calls

    # Flush out our cache
    NotifyTwitter._user_cache = {}

    response_obj = {
        'screen_name': TWITTER_SCREEN_NAME,
        'id': 9876,
    }
    request.content = json.dumps(response_obj)

    obj = NotifyTwitter(
        ckey=ckey,
        csecret=csecret,
        akey=akey,
        asecret=asecret)

    assert obj.send(body="test") is True

    # Alter the key forcing us to look up a new value of ourselves again
    NotifyTwitter._user_cache = {}
    NotifyTwitter._whoami_cache = None
    obj.ckey = 'different.then.it.was'
    assert obj.send(body="test") is True

    NotifyTwitter._whoami_cache = None
    obj.ckey = 'different.again'
    assert obj.send(body="test") is True


def test_plugin_twitter_edge_cases():
    """
    NotifyTwitter() Edge Cases
    """

    with pytest.raises(TypeError):
        NotifyTwitter(
            ckey=None, csecret=None, akey=None, asecret=None)

    with pytest.raises(TypeError):
        NotifyTwitter(
            ckey='value', csecret=None, akey=None, asecret=None)

    with pytest.raises(TypeError):
        NotifyTwitter(
            ckey='value', csecret='value', akey=None, asecret=None)

    with pytest.raises(TypeError):
        NotifyTwitter(
            ckey='value', csecret='value', akey='value', asecret=None)

    assert isinstance(
        NotifyTwitter(
            ckey='value', csecret='value', akey='value', asecret='value'),
        NotifyTwitter,
    )

    assert isinstance(
        NotifyTwitter(
            ckey='value', csecret='value', akey='value', asecret='value',
            user='l2gnux'),
        NotifyTwitter,
    )

    # Invalid Target User
    with pytest.raises(TypeError):
        NotifyTwitter(
            ckey='value', csecret='value', akey='value', asecret='value',
            targets='%G@rB@g3')


def test_plugin_twitter_dm_caching(
        mocker, twitter_url,
        good_message_response, good_media_response):
    """
    Verify that the `NotifyTwitter.{_user_cache,_whoami_cache}` caches
    work as intended.
    """

    # This is the request to `account/verify_credentials.json`.
    # Explicitly mock it here so the calls to it can be evaluated.
    mock_get = mocker.patch("requests.get")
    mock_get.return_value = good_message_response

    # This test case submits two notifications, so make sure to provide two
    # mocked responses.
    mock_post = mocker.patch("requests.post")
    mock_post.side_effect = [good_message_response, good_message_response]

    # Make sure to start with empty caches.
    if hasattr(NotifyTwitter, "_user_cache"):
        NotifyTwitter._user_cache = {}
    if hasattr(NotifyTwitter, "_whoami_cache"):
        NotifyTwitter._whoami_cache = {}

    # Create application objects.
    obj = Apprise.instantiate(twitter_url)

    # Send the first notification.
    assert obj.notify(
        body='body', title='title', notify_type=NotifyType.INFO) is True

    # Test call counts.
    assert mock_get.call_count == 1
    assert mock_get.call_args_list[0][0][0] == \
        'https://api.twitter.com/1.1/account/verify_credentials.json'

    assert mock_post.call_count == 1
    assert mock_post.call_args_list[0][0][0] == \
        'https://api.twitter.com/1.1/direct_messages/events/new.json'

    # Reset the mocks to start counting calls from scratch.
    mock_get.reset_mock()
    mock_post.reset_mock()

    # Send another notification.
    assert obj.notify(
        body='body', title='title', notify_type=NotifyType.INFO) is True

    # Calls to `verify_credentials.json` will get cached by `NotifyTwitter`.
    # So, the `GET` request to `verify_credentials.json` should only have been
    # issued once.
    assert mock_get.call_count == 0
    assert mock_post.call_count == 1


def test_plugin_twitter_dm_attachments_basic(
        mocker, twitter_url,
        good_message_response, good_media_response):
    """
    NotifyTwitter() DM Attachment Checks - Basic
    """

    mock_get = mocker.patch("requests.get")
    mock_post = mocker.patch("requests.post")

    # Epoch time:
    epoch = datetime.fromtimestamp(0, timezone.utc)
    mock_get.return_value = good_message_response
    mock_post.return_value.headers = {
        'x-rate-limit-reset': (
            datetime.now(timezone.utc) - epoch).total_seconds(),
        'x-rate-limit-remaining': 1,
    }

    # The first response is for uploading the attachment,
    # the second one for posting the actual message.
    mock_post.side_effect = [good_media_response, good_message_response]

    # Create application objects.
    obj = Apprise.instantiate(twitter_url)
    attach = AppriseAttachment(os.path.join(TEST_VAR_DIR, 'apprise-test.gif'))

    # Send our notification.
    assert obj.notify(
        body='body', title='title', notify_type=NotifyType.INFO,
        attach=attach) is True

    # Test call counts.
    assert mock_get.call_count == 1
    assert mock_get.call_args_list[0][0][0] == \
        'https://api.twitter.com/1.1/account/verify_credentials.json'

    assert mock_post.call_count == 2
    assert mock_post.call_args_list[0][0][0] == \
        'https://upload.twitter.com/1.1/media/upload.json'
    assert mock_post.call_args_list[1][0][0] == \
        'https://api.twitter.com/1.1/direct_messages/events/new.json'


def test_plugin_twitter_dm_attachments_message_fails(
        mocker, twitter_url,
        good_media_response, bad_message_response):
    """
    Test case with a bad media response.
    """

    mock_post = mocker.patch("requests.post")
    mock_post.side_effect = [good_media_response, bad_message_response]

    # Create application objects.
    obj = Apprise.instantiate(twitter_url)
    attach = AppriseAttachment(os.path.join(TEST_VAR_DIR, 'apprise-test.gif'))

    # Send our notification; it will fail because of the message response.
    assert obj.notify(
        body='body', title='title', notify_type=NotifyType.INFO,
        attach=attach) is False

    assert mock_post.call_count == 2
    assert mock_post.call_args_list[0][0][0] == \
        'https://upload.twitter.com/1.1/media/upload.json'
    assert mock_post.call_args_list[1][0][0] == \
        'https://api.twitter.com/1.1/direct_messages/events/new.json'


def test_plugin_twitter_dm_attachments_upload_fails(
        mocker, twitter_url,
        good_message_response, bad_media_response):
    """
    Test case where upload fails.
    """

    mock_post = mocker.patch("requests.post")
    mock_post.side_effect = [bad_media_response, good_message_response]

    # Create application objects.
    obj = Apprise.instantiate(twitter_url)
    attach = AppriseAttachment(os.path.join(TEST_VAR_DIR, 'apprise-test.gif'))

    # Send our notification; it will fail because of the media response.
    assert obj.notify(
        body='body', title='title', notify_type=NotifyType.INFO,
        attach=attach) is False

    # Test call counts.
    assert mock_post.call_count == 1
    assert mock_post.call_args_list[0][0][0] == \
        'https://upload.twitter.com/1.1/media/upload.json'


def test_plugin_twitter_dm_attachments_invalid_attachment(
        mocker, twitter_url, good_message_response):
    """
    Test case with an invalid attachment.
    """

    mock_post: Mock = mocker.patch("requests.post")
    mock_post.side_effect = [good_media_response, good_message_response]

    # Create application objects.
    # An invalid attachment will cause a failure.
    obj = Apprise.instantiate(twitter_url)
    attach = AppriseAttachment(
        os.path.join(TEST_VAR_DIR, '/invalid/path/to/an/invalid/file.jpg'))

    assert obj.notify(
        body='body', title='title', notify_type=NotifyType.INFO,
        attach=attach) is False

    # Verify no post requests have been issued, because attachment is not good.
    assert mock_post.mock_calls == []


def test_plugin_twitter_dm_attachments_multiple(
        mocker, twitter_url,
        good_message_response, good_media_response):

    mock_post = mocker.patch("requests.post")

    mock_post.side_effect = [
        good_media_response, good_media_response, good_media_response,
        good_media_response, good_message_response, good_message_response,
        good_message_response, good_message_response]

    # 4 images are produced
    attach = [
        os.path.join(TEST_VAR_DIR, 'apprise-test.gif'),
        os.path.join(TEST_VAR_DIR, 'apprise-test.gif'),
        os.path.join(TEST_VAR_DIR, 'apprise-test.jpeg'),
        os.path.join(TEST_VAR_DIR, 'apprise-test.png'),
        # This one is not supported, so it's ignored gracefully
        os.path.join(TEST_VAR_DIR, 'apprise-test.mp4'),
    ]

    # Create application objects.
    obj = Apprise.instantiate(twitter_url)

    assert obj.notify(
        body='body', title='title', notify_type=NotifyType.INFO,
        attach=attach) is True

    assert mock_post.call_count == 8
    assert mock_post.call_args_list[0][0][0] == \
        'https://upload.twitter.com/1.1/media/upload.json'
    assert mock_post.call_args_list[1][0][0] == \
        'https://upload.twitter.com/1.1/media/upload.json'
    assert mock_post.call_args_list[2][0][0] == \
        'https://upload.twitter.com/1.1/media/upload.json'
    assert mock_post.call_args_list[3][0][0] == \
        'https://upload.twitter.com/1.1/media/upload.json'
    assert mock_post.call_args_list[4][0][0] == \
        'https://api.twitter.com/1.1/direct_messages/events/new.json'
    assert mock_post.call_args_list[5][0][0] == \
        'https://api.twitter.com/1.1/direct_messages/events/new.json'
    assert mock_post.call_args_list[6][0][0] == \
        'https://api.twitter.com/1.1/direct_messages/events/new.json'
    assert mock_post.call_args_list[7][0][0] == \
        'https://api.twitter.com/1.1/direct_messages/events/new.json'


def test_plugin_twitter_dm_attachments_multiple_oserror(
        mocker, twitter_url,
        good_message_response, good_media_response):

    # Inject an `OSError` into the middle of the operation.
    mock_post = mocker.patch("requests.post")
    mock_post.side_effect = [good_media_response, OSError()]

    # 2 images are produced
    attach = [
        os.path.join(TEST_VAR_DIR, 'apprise-test.gif'),
        os.path.join(TEST_VAR_DIR, 'apprise-test.png'),
        # This one is not supported, so it's ignored gracefully
        os.path.join(TEST_VAR_DIR, 'apprise-test.mp4'),
    ]

    # Create application objects.
    obj = Apprise.instantiate(twitter_url)

    # We'll fail to send this time
    assert obj.notify(
        body='body', title='title', notify_type=NotifyType.INFO,
        attach=attach) is False

    assert mock_post.call_count == 2
    assert mock_post.call_args_list[0][0][0] == \
        'https://upload.twitter.com/1.1/media/upload.json'
    assert mock_post.call_args_list[1][0][0] == \
        'https://upload.twitter.com/1.1/media/upload.json'


@patch('requests.post')
def test_plugin_twitter_tweet_attachments_basic(
        mock_post, twitter_url, good_message_response, good_media_response):
    """
    NotifyTwitter() Tweet Attachment Checks - Basic
    """

    mock_post.side_effect = [good_media_response, good_message_response]

    # Create application objects.
    twitter_url += '?mode=tweet'
    obj = Apprise.instantiate(twitter_url)
    attach = AppriseAttachment(os.path.join(TEST_VAR_DIR, 'apprise-test.gif'))

    # Send our notification
    assert obj.notify(
        body='body', title='title', notify_type=NotifyType.INFO,
        attach=attach) is True

    # Verify API calls.
    assert mock_post.call_count == 2
    assert mock_post.call_args_list[0][0][0] == \
        'https://upload.twitter.com/1.1/media/upload.json'
    assert mock_post.call_args_list[1][0][0] == \
        'https://api.twitter.com/1.1/statuses/update.json'


@patch('requests.post')
def test_plugin_twitter_tweet_attachments_more_logging(
        mock_post, twitter_url, good_media_response):
    """
    NotifyTwitter() Tweet Attachment Checks - More logging

    TODO: The "more logging" aspect is not verified yet?
    """

    good_tweet_response = good_response({
        'screen_name': TWITTER_SCREEN_NAME,
        'id': 9876,
        # needed for additional logging
        'id_str': '12345',
        'user': {
            'screen_name': TWITTER_SCREEN_NAME,
        }
    })

    mock_post.side_effect = [good_media_response, good_tweet_response]

    # Create application objects.
    twitter_url += '?mode=tweet'
    obj = Apprise.instantiate(twitter_url)
    attach = AppriseAttachment(os.path.join(TEST_VAR_DIR, 'apprise-test.gif'))

    # Send our notification (again); this time there will be more logging
    # TODO: The "more logging" aspect is not verified yet?
    assert obj.notify(
        body='body', title='title', notify_type=NotifyType.INFO,
        attach=attach) is True

    # Verify API calls.
    assert mock_post.call_count == 2
    assert mock_post.call_args_list[0][0][0] == \
        'https://upload.twitter.com/1.1/media/upload.json'
    assert mock_post.call_args_list[1][0][0] == \
        'https://api.twitter.com/1.1/statuses/update.json'


@patch('requests.post')
def test_plugin_twitter_tweet_attachments_bad_message_response(
        mock_post, twitter_url, good_media_response, bad_message_response):

    mock_post.side_effect = [good_media_response, bad_message_response]

    # Create application objects.
    twitter_url += '?mode=tweet'
    obj = Apprise.instantiate(twitter_url)
    attach = AppriseAttachment(os.path.join(TEST_VAR_DIR, 'apprise-test.gif'))

    # Our notification will fail now since our tweet will error out.
    assert obj.notify(
        body='body', title='title', notify_type=NotifyType.INFO,
        attach=attach) is False

    # Verify API calls.
    assert mock_post.call_count == 2
    assert mock_post.call_args_list[0][0][0] == \
        'https://upload.twitter.com/1.1/media/upload.json'
    assert mock_post.call_args_list[1][0][0] == \
        'https://api.twitter.com/1.1/statuses/update.json'


@patch('requests.post')
def test_plugin_twitter_tweet_attachments_bad_message_response_unparseable(
        mock_post, twitter_url, good_media_response):

    bad_message_response = bad_response("")
    mock_post.side_effect = [good_media_response, bad_message_response]

    # Create application objects.
    twitter_url += '?mode=tweet'
    obj = Apprise.instantiate(twitter_url)
    attach = AppriseAttachment(os.path.join(TEST_VAR_DIR, 'apprise-test.gif'))

    # The notification will fail now since the tweet will error out.
    # This is the same test as above, except that the error response is not
    # parseable.
    assert obj.notify(
        body='body', title='title', notify_type=NotifyType.INFO,
        attach=attach) is False

    # Verify API calls.
    assert mock_post.call_count == 2
    assert mock_post.call_args_list[0][0][0] == \
        'https://upload.twitter.com/1.1/media/upload.json'
    assert mock_post.call_args_list[1][0][0] == \
        'https://api.twitter.com/1.1/statuses/update.json'


@patch('requests.post')
def test_plugin_twitter_tweet_attachments_upload_fails(
        mock_post, twitter_url, good_media_response):

    # Prepare a bad tweet response.
    bad_tweet_response = bad_response({})

    # Test case where upload fails.
    mock_post.side_effect = [good_media_response, bad_tweet_response]

    # Create application objects.
    twitter_url += '?mode=tweet'
    obj = Apprise.instantiate(twitter_url)
    attach = AppriseAttachment(os.path.join(TEST_VAR_DIR, 'apprise-test.gif'))

    # Send our notification; it will fail because of the message response.
    assert obj.notify(
        body='body', title='title', notify_type=NotifyType.INFO,
        attach=attach) is False

    # Verify API calls.
    assert mock_post.call_count == 2
    assert mock_post.call_args_list[0][0][0] == \
        'https://upload.twitter.com/1.1/media/upload.json'
    assert mock_post.call_args_list[1][0][0] == \
        'https://api.twitter.com/1.1/statuses/update.json'


@patch('requests.post')
def test_plugin_twitter_tweet_attachments_invalid_attachment(
        mock_post, twitter_url, good_message_response, good_media_response):

    mock_post.side_effect = [good_media_response, good_message_response]

    # Create application objects.
    twitter_url += '?mode=tweet'
    obj = Apprise.instantiate(twitter_url)
    attach = AppriseAttachment(
        os.path.join(TEST_VAR_DIR, '/invalid/path/to/an/invalid/file.jpg'))

    # An invalid attachment will cause a failure.
    assert obj.notify(
        body='body', title='title', notify_type=NotifyType.INFO,
        attach=attach) is False

    # No post request as attachment is not good.
    assert mock_post.call_count == 0


@patch('requests.post')
def test_plugin_twitter_tweet_attachments_multiple_batch(
        mock_post, twitter_url, good_message_response, good_media_response):

    mock_post.side_effect = [
        good_media_response, good_media_response, good_media_response,
        good_media_response, good_message_response, good_message_response,
        good_message_response, good_message_response]

    # instantiate our object
    obj = Apprise.instantiate(twitter_url + "?mode=tweet")

    # 4 images are produced
    attach = [
        os.path.join(TEST_VAR_DIR, 'apprise-test.gif'),
        os.path.join(TEST_VAR_DIR, 'apprise-test.gif'),
        os.path.join(TEST_VAR_DIR, 'apprise-test.jpeg'),
        os.path.join(TEST_VAR_DIR, 'apprise-test.png'),
        # This one is not supported, so it's ignored gracefully
        os.path.join(TEST_VAR_DIR, 'apprise-test.mp4'),
    ]

    assert obj.notify(
        body='body', title='title', notify_type=NotifyType.INFO,
        attach=attach) is True

    assert mock_post.call_count == 7
    assert mock_post.call_args_list[0][0][0] == \
        'https://upload.twitter.com/1.1/media/upload.json'
    assert mock_post.call_args_list[1][0][0] == \
        'https://upload.twitter.com/1.1/media/upload.json'
    assert mock_post.call_args_list[2][0][0] == \
        'https://upload.twitter.com/1.1/media/upload.json'
    assert mock_post.call_args_list[3][0][0] == \
        'https://upload.twitter.com/1.1/media/upload.json'
    assert mock_post.call_args_list[4][0][0] == \
        'https://api.twitter.com/1.1/statuses/update.json'
    assert mock_post.call_args_list[5][0][0] == \
        'https://api.twitter.com/1.1/statuses/update.json'
    # The 2 images are grouped together (batch mode)
    assert mock_post.call_args_list[6][0][0] == \
        'https://api.twitter.com/1.1/statuses/update.json'


@patch('requests.post')
def test_plugin_twitter_tweet_attachments_multiple_nobatch(
        mock_post, twitter_url, good_message_response, good_media_response):

    mock_post.side_effect = [
        good_media_response, good_media_response, good_media_response,
        good_media_response, good_message_response, good_message_response,
        good_message_response, good_message_response]

    # instantiate our object (without a batch mode)
    obj = Apprise.instantiate(twitter_url + "?mode=tweet&batch=no")

    # 4 images are produced
    attach = [
        os.path.join(TEST_VAR_DIR, 'apprise-test.gif'),
        os.path.join(TEST_VAR_DIR, 'apprise-test.gif'),
        os.path.join(TEST_VAR_DIR, 'apprise-test.jpeg'),
        os.path.join(TEST_VAR_DIR, 'apprise-test.png'),
        # This one is not supported, so it's ignored gracefully
        os.path.join(TEST_VAR_DIR, 'apprise-test.mp4'),
    ]

    assert obj.notify(
        body='body', title='title', notify_type=NotifyType.INFO,
        attach=attach) is True

    assert mock_post.call_count == 8
    assert mock_post.call_args_list[0][0][0] == \
        'https://upload.twitter.com/1.1/media/upload.json'
    assert mock_post.call_args_list[1][0][0] == \
        'https://upload.twitter.com/1.1/media/upload.json'
    assert mock_post.call_args_list[2][0][0] == \
        'https://upload.twitter.com/1.1/media/upload.json'
    assert mock_post.call_args_list[3][0][0] == \
        'https://upload.twitter.com/1.1/media/upload.json'
    assert mock_post.call_args_list[4][0][0] == \
        'https://api.twitter.com/1.1/statuses/update.json'
    assert mock_post.call_args_list[5][0][0] == \
        'https://api.twitter.com/1.1/statuses/update.json'
    assert mock_post.call_args_list[6][0][0] == \
        'https://api.twitter.com/1.1/statuses/update.json'
    assert mock_post.call_args_list[7][0][0] == \
        'https://api.twitter.com/1.1/statuses/update.json'


@patch('requests.post')
def test_plugin_twitter_tweet_attachments_multiple_oserror(
        mock_post, twitter_url, good_media_response):

    # We have an OSError thrown in the middle of our preparation
    mock_post.side_effect = [good_media_response, OSError()]

    # 2 images are produced
    attach = [
        os.path.join(TEST_VAR_DIR, 'apprise-test.gif'),
        os.path.join(TEST_VAR_DIR, 'apprise-test.png'),
        # This one is not supported, so it's ignored gracefully
        os.path.join(TEST_VAR_DIR, 'apprise-test.mp4'),
    ]

    # We'll fail to send this time
    obj = Apprise.instantiate(twitter_url + "?mode=tweet")
    assert obj.notify(
        body='body', title='title', notify_type=NotifyType.INFO,
        attach=attach) is False

    assert mock_post.call_count == 2
    assert mock_post.call_args_list[0][0][0] == \
        'https://upload.twitter.com/1.1/media/upload.json'
    assert mock_post.call_args_list[1][0][0] == \
        'https://upload.twitter.com/1.1/media/upload.json'
