# -*- coding: utf-8 -*-
#
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

import os
import sys
from unittest import mock

import pytest
import requests
from json import dumps
from datetime import datetime
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
import logging
logging.disable(logging.CRITICAL)

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

# 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': 'twitter://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 test_plugin_twitter_urls():
    """
    NotifyTwitter() Apprise URLs

    """

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


@mock.patch('requests.get')
@mock.patch('requests.post')
def test_plugin_twitter_general(mock_post, mock_get):
    """
    NotifyTwitter() General Tests

    """
    ckey = 'ckey'
    csecret = 'csecret'
    akey = 'akey'
    asecret = 'asecret'
    screen_name = 'apprise'

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

    # Epoch time:
    epoch = datetime.utcfromtimestamp(0)

    request = mock.Mock()
    request.content = dumps(response_obj)
    request.status_code = requests.codes.ok
    request.headers = {
        'x-rate-limit-reset': (datetime.utcnow() - 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=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.utcnow() - 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.utcnow() - 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.utcnow() - epoch).total_seconds()
    request.headers['x-rate-limit-remaining'] = 1
    obj.ratelimit_remaining = 1

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

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

    # Flush our cache forcing it's 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, screen_name))
    assert isinstance(results, dict) is True
    assert 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': screen_name,
        'id': 9876,
    }
    request.content = 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')


@pytest.mark.skipif(
    hasattr(sys, "pypy_version_info") and sys.version_info < (3, 7),
    reason="Does not work or is flaky on PyPy 3.6")
@mock.patch('requests.post')
@mock.patch('requests.get')
def test_plugin_twitter_dm_attachments(mock_get, mock_post):
    """
    NotifyTwitter() DM Attachment Checks

    """
    ckey = 'ckey'
    csecret = 'csecret'
    akey = 'akey'
    asecret = 'asecret'
    screen_name = 'apprise'

    good_dm_response_obj = {
        'screen_name': screen_name,
        'id': 9876,
    }

    # Epoch time:
    epoch = datetime.utcfromtimestamp(0)

    # Prepare a good DM response
    good_dm_response = mock.Mock()
    good_dm_response.content = dumps(good_dm_response_obj)
    good_dm_response.status_code = requests.codes.ok
    good_dm_response.headers = {
        'x-rate-limit-reset': (datetime.utcnow() - epoch).total_seconds(),
        'x-rate-limit-remaining': 1,
    }

    # Prepare bad response
    bad_response = mock.Mock()
    bad_response.content = dumps({})
    bad_response.status_code = requests.codes.internal_server_error

    # Prepare a good media response
    good_media_response = mock.Mock()
    good_media_response.content = 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
        }
    })
    good_media_response.status_code = requests.codes.ok

    # Prepare a bad media response
    bad_media_response = mock.Mock()
    bad_media_response.content = dumps({
        "errors": [
            {
                "code": 93,
                "message": "This application is not allowed to access or "
                "delete your direct messages.",
            }]})
    bad_media_response.status_code = requests.codes.internal_server_error

    mock_post.side_effect = [good_media_response, good_dm_response]
    mock_get.return_value = good_dm_response

    twitter_url = 'twitter://{}/{}/{}/{}'.format(ckey, csecret, akey, asecret)

    # attach our content
    attach = AppriseAttachment(os.path.join(TEST_VAR_DIR, 'apprise-test.gif'))

    # instantiate our object
    obj = Apprise.instantiate(twitter_url)

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

    # Test our call count
    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'

    mock_get.reset_mock()
    mock_post.reset_mock()

    # Test case where upload fails
    mock_get.return_value = good_dm_response
    mock_post.side_effect = [bad_media_response, good_dm_response]

    # instantiate our object
    obj = Apprise.instantiate(twitter_url)

    # 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 our call count
    assert mock_get.call_count == 0
    # No get request as cached response is used
    assert mock_post.call_count == 1
    assert mock_post.call_args_list[0][0][0] == \
        'https://upload.twitter.com/1.1/media/upload.json'

    mock_get.reset_mock()
    mock_post.reset_mock()

    # Test case where upload fails
    mock_get.return_value = good_dm_response
    mock_post.side_effect = [good_media_response, bad_response]

    # instantiate our object
    obj = Apprise.instantiate(twitter_url)

    # 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

    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'

    # No get request as cached response is used
    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'

    mock_get.reset_mock()
    mock_post.reset_mock()

    mock_post.side_effect = [good_media_response, good_dm_response]
    mock_get.return_value = good_dm_response

    # An invalid attachment will cause a failure
    path = 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=path) is False

    # No get request as cached response is used
    assert mock_get.call_count == 0

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

    mock_get.reset_mock()
    mock_post.reset_mock()

    mock_post.side_effect = [
        good_media_response, good_media_response, good_media_response,
        good_media_response, good_dm_response, good_dm_response,
        good_dm_response, good_dm_response]
    mock_get.return_value = good_dm_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'),
    ]

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

    assert mock_get.call_count == 0
    # No get request as cached response is used
    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'

    mock_get.reset_mock()
    mock_post.reset_mock()

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

    # 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
    assert obj.notify(
        body='body', title='title', notify_type=NotifyType.INFO,
        attach=attach) is False

    assert mock_get.call_count == 0
    # No get request as cached response is used
    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'


@mock.patch('requests.post')
@mock.patch('requests.get')
def test_plugin_twitter_tweet_attachments(mock_get, mock_post):
    """
    NotifyTwitter() Tweet Attachment Checks

    """
    ckey = 'ckey'
    csecret = 'csecret'
    akey = 'akey'
    asecret = 'asecret'
    screen_name = 'apprise'

    good_tweet_response_obj = {
        'screen_name': screen_name,
        'id': 9876,
    }

    # Prepare a good DM response
    good_tweet_response = mock.Mock()
    good_tweet_response.content = dumps(good_tweet_response_obj)
    good_tweet_response.status_code = requests.codes.ok

    # Prepare bad response
    bad_response = mock.Mock()
    bad_response.content = dumps({})
    bad_response.status_code = requests.codes.internal_server_error

    # Prepare a good media response
    good_media_response = mock.Mock()
    good_media_response.content = 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
        }
    })
    good_media_response.status_code = requests.codes.ok

    # Prepare a bad media response
    bad_media_response = mock.Mock()
    bad_media_response.content = dumps({
        "errors": [
            {
                "code": 93,
                "message": "This application is not allowed to access or "
                "delete your direct messages.",
            }]})
    bad_media_response.status_code = requests.codes.internal_server_error

    mock_post.side_effect = [good_media_response, good_tweet_response]
    mock_get.return_value = good_tweet_response

    twitter_url = 'twitter://{}/{}/{}/{}?mode=tweet'.format(
        ckey, csecret, akey, asecret)

    # attach our content
    attach = AppriseAttachment(os.path.join(TEST_VAR_DIR, 'apprise-test.gif'))

    # instantiate our object
    obj = Apprise.instantiate(twitter_url)

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

    # Test our call count
    assert mock_get.call_count == 0
    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'

    # Update our good response to have more details in it
    good_tweet_response_obj = {
        'screen_name': screen_name,
        'id': 9876,
        # needed for additional logging
        'id_str': '12345',
        'user': {
            'screen_name': screen_name,
        }
    }

    good_tweet_response.content = dumps(good_tweet_response_obj)

    mock_get.reset_mock()
    mock_post.reset_mock()

    mock_post.side_effect = [good_media_response, good_tweet_response]
    mock_get.return_value = good_tweet_response

    # instantiate our object
    obj = Apprise.instantiate(twitter_url)

    # Send our notification (again); this time there willb e more tweet logging
    assert obj.notify(
        body='body', title='title', notify_type=NotifyType.INFO,
        attach=attach) is True

    # Test our call count
    assert mock_get.call_count == 0
    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'

    mock_get.reset_mock()
    mock_post.reset_mock()

    mock_post.side_effect = [good_media_response, bad_media_response]

    # instantiate our object
    obj = Apprise.instantiate(twitter_url)

    # 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

    # Test our call count
    assert mock_get.call_count == 0
    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'

    mock_get.reset_mock()
    mock_post.reset_mock()

    bad_media_response.content = ''

    mock_post.side_effect = [good_media_response, bad_media_response]

    # instantiate our object
    obj = Apprise.instantiate(twitter_url)

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

    # Test our call count
    assert mock_get.call_count == 0
    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'

    mock_get.reset_mock()
    mock_post.reset_mock()

    # Test case where upload fails
    mock_get.return_value = good_tweet_response
    mock_post.side_effect = [good_media_response, bad_response]

    # instantiate our object
    obj = Apprise.instantiate(twitter_url)

    # 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

    assert mock_get.call_count == 0
    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'

    mock_get.reset_mock()
    mock_post.reset_mock()

    mock_post.side_effect = [good_media_response, good_tweet_response]
    mock_get.return_value = good_tweet_response

    # An invalid attachment will cause a failure
    path = 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=path) is False

    # No get request as cached response is used
    assert mock_get.call_count == 0

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

    mock_get.reset_mock()
    mock_post.reset_mock()

    mock_post.side_effect = [
        good_media_response, good_media_response, good_media_response,
        good_media_response, good_tweet_response, good_tweet_response,
        good_tweet_response, good_tweet_response]
    mock_get.return_value = good_tweet_response

    # instantiate our object (without a batch mode)
    obj = Apprise.instantiate(twitter_url + "&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_get.call_count == 0
    # No get request as cached response is used
    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'

    mock_get.reset_mock()
    mock_post.reset_mock()

    mock_post.side_effect = [
        good_media_response, good_media_response, good_media_response,
        good_media_response, good_tweet_response, good_tweet_response,
        good_tweet_response, good_tweet_response]
    mock_get.return_value = good_tweet_response

    # instantiate our object
    obj = Apprise.instantiate(twitter_url)

    # 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_get.call_count == 0
    # No get request as cached response is used
    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'

    mock_get.reset_mock()
    mock_post.reset_mock()

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

    # 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
    assert obj.notify(
        body='body', title='title', notify_type=NotifyType.INFO,
        attach=attach) is False

    assert mock_get.call_count == 0
    # No get request as cached response is used
    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'
