from contextlib import nested
import json

from requests.exceptions import ConnectionError, Timeout
from mock import ANY, Mock, patch

from basket import (BasketException, confirm, confirm_email_change, debug_user,
                    errors, get_newsletters, lookup_user, request, send_recovery_message,
                    start_email_change, subscribe, send_sms,
                    unsubscribe, update_user, user)
from basket.base import basket_url, get_env_or_setting, parse_response

try:
    import unittest2 as unittest
except ImportError:
    import unittest


# Warning: there are two request() methods, one in basket-client and
# one in the requests library. Pay attention, it's very confusing.

class TestBasketClient(unittest.TestCase):

    def test_basket_url_no_token(self):
        """Form basket URL properly when no token used"""
        with patch('basket.base.BASKET_URL', new="BASKET_URL"):
            result = basket_url("METHOD")
        self.assertEqual("BASKET_URL/news/METHOD/", result)

    def test_basket_url_with_token(self):
        """Form basket URL properly when token used"""
        with patch('basket.base.BASKET_URL', new="BASKET_URL"):
            result = basket_url("METHOD", "TOKEN")
        self.assertEqual("BASKET_URL/news/METHOD/TOKEN/", result)

    def test_response_not_200(self):
        """parse_response() raises exception on non-200 status code"""
        # and puts the status code on the exception
        res = Mock(status_code=666)
        try:
            parse_response(res)
        except BasketException as e:
            self.assertEqual(666, e.status_code)
        else:
            self.fail("parse_response should have raised BasketException")

    def test_response_error(self):
        """parse_response() raises exception on status=error"""
        content = json.dumps({'status': 'error', 'desc': 'ERROR', 'code': 3})
        res = Mock(status_code=200, content=content,
                   content_type='application/json')
        try:
            parse_response(res)
        except BasketException as e:
            self.assertEqual(3, e.code)
        else:
            self.fail("parse_response should have raised BasketException")

    def test_response_error_no_code(self):
        """if response has no code, and is error, then the code in
        the exception is the UNKNOWN code"""
        content = json.dumps({'status': 'error', 'desc': 'ERROR'})
        res = Mock(status_code=200, content=content,
                   content_type='application/json')
        try:
            parse_response(res)
        except BasketException as e:
            self.assertEqual(errors.BASKET_UNKNOWN_ERROR, e.code)
        else:
            self.fail("parse_response should have raised BasketException")

    def test_response_content(self):
        """parse_response() returns parsed response content if no error"""
        data = {u'status': u'ok', u'foo': u'bar'}
        content = json.dumps(data)
        res = Mock(status_code=200, content=content,
                   content_type='application/json')
        result = parse_response(res)
        self.assertEqual(data, result)

    def test_response_no_content_type(self):
        """parse_response() doesn't fail if the response is missing a content type"""
        # probably only an issue in testing, but still...
        data = {u'status': u'ok', u'foo': u'bar'}
        content = json.dumps(data)
        res = Mock(status_code=200, content=content)
        result = parse_response(res)
        self.assertEqual(data, result)

    def test_request(self):
        """
        request() calls requests.request() with the expected parms
        if everything is normal, and returns the expected result.
        """
        response_data = {u'status': u'ok', u'foo': u'bar'}
        action, method, token = "ACTION", "METHOD", "TOKEN"
        url = basket_url(action, token)
        with patch('basket.base.requests.request', autospec=True) \
                as request_call:
            request_call.return_value = Mock(status_code=200,
                                             content=json.dumps(response_data),
                                             content_type='application/json')
            result = request(method, action, data="DATA",
                             token=token, params="PARAMS")

        request_call.assert_called_with(method, url, data="DATA",
                                        params="PARAMS", headers=None,
                                        timeout=ANY)
        self.assertEqual(response_data, result)

    def test_request_newsletters_string(self):
        """
        If request is passed newsletters as a string, newsletters is passed
        along unaltered.
        """
        input_data = {'newsletters': 'one,two'}
        action, method, token = "ACTION", "METHOD", "TOKEN"
        url = basket_url(action, token)
        content = {'one': 100, 'two': 200}
        with patch('basket.base.requests.request', autospec=True) \
                as request_call:
            request_call.return_value = Mock(status_code=200,
                                             content=json.dumps(content),
                                             content_type='application/json')
            result = request(method, action, data=input_data,
                             token=token, params="PARAMS")

        request_call.assert_called_with(method, url, data=input_data,
                                        params="PARAMS", headers=None,
                                        timeout=ANY)
        self.assertEqual(content, result)

    def test_request_newsletters_non_string(self):
        """
        If request is passed newsletters as a non-string, newsletters is
        converted to a comma-separated string
        """
        response_data = {u'status': u'ok', u'foo': u'bar'}
        input_data = {'newsletters': ['one', 'two'], 'thing': 1}
        expected_input_data = input_data.copy()
        newsletters = ','.join(input_data['newsletters'])
        expected_input_data['newsletters'] = newsletters
        action, method, token = "ACTION", "METHOD", "TOKEN"
        url = basket_url(action, token)
        with patch('basket.base.requests.request', autospec=True) \
                as request_call:
            request_call.return_value = Mock(status_code=200,
                                             content=json.dumps(response_data),
                                             content_type='application/json')
            result = request(method, action, data=input_data,
                             token=token, params="PARAMS")

        request_call.assert_called_with(method, url, data=expected_input_data,
                                        params="PARAMS", headers=None,
                                        timeout=ANY)
        self.assertEqual(response_data, result)

    def test_request_conn_error(self):
        """
        If requests throws a ConnectionError, it's converted to
        a BasketException
        """
        input_data = {'newsletters': ['one', 'two'], 'thing': 1}
        expected_input_data = input_data.copy()
        newsletters = ','.join(input_data['newsletters'])
        expected_input_data['newsletters'] = newsletters
        action, method, token = "ACTION", "METHOD", "TOKEN"
        with patch('basket.base.requests.request', autospec=True) \
                as request_call:
            request_call.side_effect = ConnectionError
            with self.assertRaises(BasketException):
                request(method, action, data=input_data,
                        token=token, params="PARAMS")

    def test_request_timeout(self):
        """
        If requests times out, it's converted to
        a BasketException
        """
        input_data = {'newsletters': ['one', 'two'], 'thing': 1}
        expected_input_data = input_data.copy()
        newsletters = ','.join(input_data['newsletters'])
        expected_input_data['newsletters'] = newsletters
        action, method, token = "ACTION", "METHOD", "TOKEN"
        with patch('basket.base.requests.request', autospec=True) \
                as request_call:
            request_call.side_effect = Timeout
            with self.assertRaises(BasketException):
                request(method, action, data=input_data,
                        token=token, params="PARAMS")

    def test_subscribe(self):
        """
        subscribe calls request with the expected parms and returns the result
        """
        email = "user1@example.com"
        newsletters = ['news1', 'news2']
        kwargs = {
            'arg1': 100,
            'arg2': 200,
        }
        expected_kwargs = kwargs.copy()
        expected_kwargs['email'] = email
        expected_kwargs['newsletters'] = newsletters
        with patch('basket.base.request', autospec=True) as request_call:
            result = subscribe(email, newsletters, **kwargs)

        request_call.assert_called_with('post', 'subscribe',
                                        data=expected_kwargs,
                                        headers={})
        self.assertEqual(request_call.return_value, result)

    def test_subscribe_sync_y_api_key_in_args(self):
        """
        subscribe calls request with the expected parms and returns the result
        when sync='Y', adds the API key from the args to the headers
        """
        api_key = 'foo_bar'
        email = "user1@example.com"
        newsletters = ['news1', 'news2']
        kwargs = {
            'arg1': 100,
            'arg2': 200,
            'sync': 'Y',
            'api_key': api_key,
        }
        expected_kwargs = kwargs.copy()
        expected_kwargs['email'] = email
        expected_kwargs['newsletters'] = newsletters
        del expected_kwargs['api_key']
        with patch('basket.base.request', autospec=True) as request_call:
            result = subscribe(email, newsletters, **kwargs)

        request_call.assert_called_with('post', 'subscribe',
                                        data=expected_kwargs,
                                        headers={'x-api-key': api_key})
        self.assertEqual(request_call.return_value, result)

    def test_subscribe_sync_y_api_key_not_in_args(self):
        """
        subscribe calls request with the expected parms and returns the result
        when sync='Y', adds the API key from the settings to the headers
        """
        api_key = 'foo_bar'
        email = "user1@example.com"
        newsletters = ['news1', 'news2']
        kwargs = {
            'arg1': 100,
            'arg2': 200,
            'sync': 'Y',
        }
        expected_kwargs = kwargs.copy()
        expected_kwargs['email'] = email
        expected_kwargs['newsletters'] = newsletters
        with nested(patch('basket.base.request', autospec=True),
                    patch('basket.base.BASKET_API_KEY', api_key)) \
                as (request_call, API_KEY):
            result = subscribe(email, newsletters, **kwargs)

        request_call.assert_called_with('post', 'subscribe',
                                        data=expected_kwargs,
                                        headers={'x-api-key': api_key})
        self.assertEqual(request_call.return_value, result)

    def test_unsubscribe(self):
        """
        unsubscribe calls request with the expected parms, returns the result
        """
        email = "user1@example.com"
        newsletters = ['news1', 'news2']
        token = "TOKEN"
        optout = False
        expected_data = {
            'email': email,
            'newsletters': newsletters,
        }
        with patch('basket.base.request', autospec=True) as request_call:
            result = unsubscribe(token, email, newsletters, optout)

        request_call.assert_called_with('post', 'unsubscribe',
                                        data=expected_data,
                                        token=token)
        self.assertEqual(request_call.return_value, result)

    def test_unsubscribe_optout(self):
        """
        unsubscribe calls request with the expected parms and returns the
        result. optout is passed if true, instead of newsletters.
        """
        email = "user1@example.com"
        newsletters = ['news1', 'news2']
        token = "TOKEN"
        optout = True
        expected_data = {
            'email': email,
            'optout': 'Y'
        }
        with patch('basket.base.request', autospec=True) as request_call:
            result = unsubscribe(token, email, newsletters, optout)

        request_call.assert_called_with('post', 'unsubscribe',
                                        data=expected_data,
                                        token=token)
        self.assertEqual(request_call.return_value, result)

    def test_unsubscribe_bad_args(self):
        """
        unsubscribe must be passed newsletters or optout, or it raises
        BasketException
        """
        email = "user1@example.com"
        newsletters = None
        token = "TOKEN"
        optout = None
        with patch('basket.base.request', autospec=True):
            with self.assertRaises(BasketException):
                unsubscribe(token, email, newsletters, optout)

    def test_user(self):
        """
        user passes the expected args to request() and returns the result.
        """
        token = "TOKEN"
        with patch('basket.base.request', autospec=True) as request_call:
            result = user(token)
        request_call.assert_called_with('get', 'user', token=token)
        self.assertEqual(request_call.return_value, result)

    def test_update_user(self):
        """
        update_user passes the expected args to request(), returns the result.
        """
        token = "TOKEN"
        kwargs = {
            'one': 100,
            'two': 200,
        }
        with patch('basket.base.request', autospec=True) as request_call:
            result = update_user(token, **kwargs)
        request_call.assert_called_with('post', 'user', data=kwargs,
                                        token=token)
        self.assertEqual(request_call.return_value, result)

    def test_debug_user(self):
        """
        debug_user passes the expected args to request(), returns the result.
        """
        email = "user@example.com"
        supertoken = "STOKEN"
        params = {'email': email, 'supertoken': supertoken}
        with patch('basket.base.request', autospec=True) as request_call:
            result = debug_user(email, supertoken)
        request_call.assert_called_with('get', 'debug-user', params=params)
        self.assertEqual(request_call.return_value, result)

    def test_get_newsletters(self):
        """
        get_newsletters passes the expected args to request() and returns
        the 'newsletters' part of what it returns
        """
        with patch('basket.base.request', autospec=True) as request_call:
            request_call.return_value = {'newsletters': 'FOO BAR'}
            result = get_newsletters()
        request_call.assert_called_with('get', 'newsletters')
        self.assertEqual('FOO BAR', result)

    def test_confirm(self):
        """
        confirm() passes the expected args to request, and returns the result.
        """
        token = "TOKEN"
        with patch('basket.base.request', autospec=True) as request_call:
            result = confirm(token)
        request_call.assert_called_with('post', 'confirm', token=token)
        self.assertEqual(request_call.return_value, result)

    def test_send_recovery_email(self):
        """
        send_recovery_email() passes the expected args to request,
        and returns the result.
        """
        email = "dude@example.com"
        with patch('basket.base.request', autospec=True) as mock_request:
            result = send_recovery_message(email)
        data = {'email': email}
        mock_request.assert_called_with('post', 'recover', data=data)
        self.assertEqual(mock_request.return_value, result)

    @patch('basket.base.request')
    def test_lookup_user_token(self, mock_request):
        """Calling lookup_user with a token should not require an API key."""
        lookup_user(token='TOKEN')
        mock_request.assert_called_with('get', 'lookup-user',
                                        params={'token': 'TOKEN'})

    @patch('basket.base.request')
    def test_lookup_user_email(self, mock_request):
        """Calling lookup_user with email and api key should succeed."""
        api_key = 'There is only XUL!'
        email = 'dana@example.com'
        lookup_user(email=email, api_key=api_key)
        mock_request.assert_called_with('get', 'lookup-user',
                                        params={'email': email},
                                        headers={'x-api-key': api_key})

    @patch('basket.base.request')
    def test_lookup_user_email_setting(self, mock_request):
        """Calling lookup_user with email and api key setting should succeed."""
        api_key = 'There is only XUL!'
        email = 'dana@example.com'
        with patch('basket.base.BASKET_API_KEY', api_key):
            lookup_user(email=email)
        mock_request.assert_called_with('get', 'lookup-user',
                                        params={'email': email},
                                        headers={'x-api-key': api_key})

    @patch('basket.base.request')
    def test_lookup_user_no_api_key(self, mock_request):
        """Calling lookup_user with email and no api key raises an exception."""
        with self.assertRaises(BasketException):
            lookup_user(email='dana@example.com')

        self.assertFalse(mock_request.called)

    @patch('basket.base.request')
    def test_lookup_user_no_args(self, mock_request):
        """Calling lookup_user with no email or token raises an exception."""
        with self.assertRaises(BasketException):
            lookup_user()

        self.assertFalse(mock_request.called)

    @patch('basket.base.settings',
           type('X', (object,), {'TESTING_SETTINGS': True}))
    def test_gets_setting(self):
        """Should return the setting if it exists."""
        self.assertEqual(get_env_or_setting('TESTING_SETTINGS'), True)
        self.assertEqual(get_env_or_setting('DOES_NOT_EXIST'), None)

    def test_get_setting_from_env(self):
        """Should always return the value from the env if it exists."""
        self.assertEqual(get_env_or_setting('TESTING_SETTINGS'), None)
        with patch('basket.base.settings',
                   type('X', (object,), {'TESTING_SETTINGS': 'DUDE'})):
            self.assertEqual(get_env_or_setting('TESTING_SETTINGS'), 'DUDE')
            with patch.dict('os.environ', {'TESTING_SETTINGS': 'WALTER'}):
                self.assertEqual(get_env_or_setting('TESTING_SETTINGS'),
                                 'WALTER')

    def test_start_email_change(self):
        """
        start_email_change() passes the expected args to request, and returns the result.
        """
        token = "TOKEN"
        new_email = "NEW EMAIL"
        with patch('basket.base.request', autospec=True) as request_call:
            result = start_email_change(token, new_email)
        request_call.assert_called_with('post', 'start-email-change', token=token, data={'email': new_email})
        self.assertEqual(request_call.return_value, result)

    def test_confirm_email_change(self):
        """
        confirm_email_change() passes the expected args to request, and returns the result.
        """
        change_key = "CHANGE KEY"
        with patch('basket.base.request', autospec=True) as request_call:
            result = confirm_email_change(change_key)
        request_call.assert_called_with('post', 'confirm-email-change', token=change_key)
        self.assertEqual(request_call.return_value, result)

    @patch('basket.base.request')
    def test_subscribe_source_ip(self, mock_request):
        subscribe('dude@example.com', 'abiding-times', source_ip='1.1.1.1')
        mock_request.assert_called_with('post', 'subscribe',
                                        data={'email': 'dude@example.com',
                                              'newsletters': 'abiding-times'},
                                        headers={'x-source-ip': '1.1.1.1'})

    @patch('basket.base.request')
    def test_send_sms_source_ip(self, mock_request):
        send_sms('5558675309', 'abide', source_ip='1.1.1.1')
        mock_request.assert_called_with('post', 'subscribe_sms',
                                        data={'mobile_number': '5558675309',
                                              'msg_name': 'abide',
                                              'optin': 'N'},
                                        headers={'x-source-ip': '1.1.1.1'})

    @patch('basket.base.requests')
    def test_mock_success(self, mock_requests):
        result = subscribe('success@example.com', 'abiding-times')
        self.assertEqual(result, {'status': 'ok'})
        self.assertFalse(mock_requests.request.called)

    @patch('basket.base.requests')
    def test_mock_failure(self, mock_requests):
        with self.assertRaises(BasketException) as exc:
            subscribe('failure@example.com', 'abiding-times')
        self.assertEqual(exc.exception.code, errors.BASKET_MOCK_FAILURE)
        self.assertFalse(mock_requests.request.called)
