# -*- coding: utf-8 -*-
"""
Test 'pymonzo.monzo_api' file
"""
from __future__ import unicode_literals

import codecs
import json
import os
import tempfile

import pytest
from six.moves.urllib.parse import urljoin

from pymonzo import MonzoAPI
from pymonzo import config
from pymonzo.api_objects import MonzoAccount, MonzoBalance, MonzoPot, MonzoTransaction


class TestMonzoAPI:
    """
    Test `monzo_api.MonzoAPI` class.
    """
    @pytest.fixture(scope='session')
    def monzo(self):
        """Helper fixture that returns a `MonzoAPI` instance"""
        return MonzoAPI(access_token='explicit_access_token')

    @pytest.fixture
    def mocked_monzo(self, mocker):
        """Helper fixture that returns a mocked `MonzoAPI` instance"""
        mocker.patch('pymonzo.monzo_api.OAuth2Session')
        mocker.patch('pymonzo.monzo_api.MonzoAPI._save_token_on_disk')

        client_id = 'explicit_client_id'
        client_secret = 'explicit_client_secret'
        auth_code = 'explicit_auth_code'

        monzo = MonzoAPI(
            client_id=client_id,
            client_secret=client_secret,
            auth_code=auth_code,
        )

        return monzo

    def test_class_initialization(self, monkeypatch, mocker):
        """
        Test class `__init__` method.
        Quite long and complicated because of the number of possible
        scenarios. Possibly to revisit in the future.
        """
        access_token = 'explicit_access_token'
        client_id = 'explicit_client_id'
        client_secret = 'explicit_client_secret'
        auth_code = 'explicit_auth_code'
        monkeypatch.setenv(config.MONZO_ACCESS_TOKEN_ENV, 'env_access_token')
        monkeypatch.setenv(config.MONZO_CLIENT_ID_ENV, 'env_client_id')
        monkeypatch.setenv(config.MONZO_CLIENT_SECRET_ENV, 'env_client_secret')
        monkeypatch.setenv(config.MONZO_AUTH_CODE_ENV, 'env_auth_code')

        # When we provide all variables both explicitly and via environment
        # variables, the explicit 'access token' should take precedence
        mocker.patch('os.path.isfile', return_value=True)
        mocked_oauth2_session = mocker.patch('pymonzo.monzo_api.OAuth2Session')
        expected_token = {
            'access_token': 'explicit_access_token',
            'token_type': 'Bearer',
        }

        monzo = MonzoAPI(
            access_token=access_token, client_id=client_id,
            client_secret=client_secret, auth_code=auth_code,
        )

        assert monzo._access_token == 'explicit_access_token'
        assert monzo._client_id is None
        assert monzo._client_secret is None
        assert monzo._auth_code is None
        assert monzo._token == expected_token
        mocked_oauth2_session.assert_called_once_with(
            client_id=None,
            token=expected_token,
        )

        # Don't pass 'access_token' explicitly
        mocked_oauth2_session = mocker.patch('pymonzo.monzo_api.OAuth2Session')
        mocked_get_oauth_token = mocker.patch(
            'pymonzo.monzo_api.MonzoAPI._get_oauth_token'
        )
        mocked_save_token_on_disk = mocker.patch(
            'pymonzo.monzo_api.MonzoAPI._save_token_on_disk'
        )
        expected_token = mocked_get_oauth_token.return_value

        monzo = MonzoAPI(
            client_id=client_id,
            client_secret=client_secret,
            auth_code=auth_code,
        )

        assert monzo._access_token is None
        assert monzo._client_id == 'explicit_client_id'
        assert monzo._client_secret == 'explicit_client_secret'
        assert monzo._auth_code == 'explicit_auth_code'
        assert monzo._token == expected_token
        mocked_get_oauth_token.assert_called_once_with()
        mocked_save_token_on_disk.assert_called_once_with()
        mocked_oauth2_session.assert_called_once_with(
            client_id='explicit_client_id',
            token=expected_token,
        )

        # Don't pass anything explicitly and the token file exists
        mocked_oauth2_session = mocker.patch('pymonzo.monzo_api.OAuth2Session')
        mocker.patch('os.path.isfile', return_value=True)
        mocked_open = mocker.patch('codecs.open', mocker.mock_open())
        mocked_json_load = mocker.patch('json.load')
        expected_token = mocked_json_load.return_value

        monzo = MonzoAPI()

        assert monzo._access_token is None
        assert monzo._client_id is expected_token['client_id']
        assert monzo._client_secret is expected_token['client_secret']
        assert monzo._auth_code is None
        assert monzo._token == expected_token
        mocked_open.assert_called_once_with(
            config.TOKEN_FILE_PATH, 'r', 'utf-8',
        )
        mocked_json_load.assert_called_once_with(mocked_open.return_value)
        mocked_get_oauth_token.assert_called_once_with()
        mocked_oauth2_session.assert_called_once_with(
            client_id=expected_token['client_id'],
            token=expected_token,
        )

        # Don't pass anything explicitly, the token file doesn't exist
        # and 'access_token' environment variable exists
        mocked_oauth2_session = mocker.patch('pymonzo.monzo_api.OAuth2Session')
        mocker.patch('os.path.isfile', return_value=False)

        expected_token = {
            'access_token': 'env_access_token',
            'token_type': 'Bearer',
        }

        monzo = MonzoAPI()

        assert monzo._access_token == 'env_access_token'
        assert monzo._client_id is None
        assert monzo._client_secret is None
        assert monzo._auth_code is None
        assert monzo._token == expected_token
        mocked_oauth2_session.assert_called_once_with(
            client_id=None,
            token=expected_token,
        )

        # Don't pass anything explicitly, the token file doesn't exist
        # and 'access_token' environment variable doesn't exist
        monkeypatch.delenv(config.MONZO_ACCESS_TOKEN_ENV)
        mocked_oauth2_session = mocker.patch('pymonzo.monzo_api.OAuth2Session')
        mocked_get_oauth_token = mocker.patch(
            'pymonzo.monzo_api.MonzoAPI._get_oauth_token'
        )
        mocked_save_token_on_disk = mocker.patch(
            'pymonzo.monzo_api.MonzoAPI._save_token_on_disk'
        )
        expected_token = mocked_get_oauth_token.return_value

        monzo = MonzoAPI()

        assert monzo._access_token is None
        assert monzo._client_id == 'env_client_id'
        assert monzo._client_secret == 'env_client_secret'
        assert monzo._auth_code == 'env_auth_code'
        assert monzo._token == expected_token
        mocked_get_oauth_token.assert_called_once_with()
        mocked_save_token_on_disk.assert_called_once_with()
        mocked_oauth2_session.assert_called_once_with(
            client_id='env_client_id',
            token=expected_token,
        )

        # None of the above
        monkeypatch.delenv(config.MONZO_CLIENT_ID_ENV)

        with pytest.raises(ValueError):
            MonzoAPI(
                auth_code=auth_code, client_id=client_id,
            )

    def test_class_save_token_on_disk_method(self, monzo):
        """Test class `_save_token_on_disk` method"""
        config.TOKEN_FILE_PATH = os.path.join(
            tempfile.gettempdir(), 'pymonzo_test',
        )

        monzo._token = {
            'foo': u'UNICODE',
            'bar': 1,
            'baz': False,
        }

        expected_token = monzo._token.copy()
        expected_token.update(client_secret=monzo._client_secret)

        monzo._save_token_on_disk()

        with codecs.open(config.TOKEN_FILE_PATH, 'r', 'utf-8') as f:
            assert json.load(f) == expected_token

    def test_class_get_oauth_token_method(self, mocker, mocked_monzo):
        """Test class `_get_oauth_token` method"""
        mocked_fetch_token = mocker.MagicMock()
        mocked_oauth2_session = mocker.patch('pymonzo.monzo_api.OAuth2Session')
        mocked_oauth2_session.return_value.fetch_token = mocked_fetch_token

        token = mocked_monzo._get_oauth_token()

        assert token == mocked_fetch_token.return_value

        mocked_oauth2_session.assert_called_once_with(
            client_id=mocked_monzo._client_id,
            redirect_uri=config.REDIRECT_URI,
        )
        mocked_fetch_token.assert_called_once_with(
            token_url=urljoin(mocked_monzo.api_url, '/oauth2/token'),
            code=mocked_monzo._auth_code,
            client_secret=mocked_monzo._client_secret,
        )

    def test_class_refresh_oath_token_method(self, mocker, mocked_monzo):
        """Test class `_refresh_oath_token` method"""
        mocked_requests_post_json = mocker.MagicMock()
        mocked_requests_post = mocker.patch('pymonzo.monzo_api.requests.post')
        mocked_requests_post.return_value.json = mocked_requests_post_json
        mocked_save_token_on_disk = mocker.patch(
            'pymonzo.monzo_api.MonzoAPI._save_token_on_disk'
        )

        expected_data = {
            'grant_type': 'refresh_token',
            'client_id': mocked_monzo._client_id,
            'client_secret': mocked_monzo._client_secret,
            'refresh_token': mocked_monzo._token['refresh_token'],
        }

        mocked_monzo._refresh_oath_token()

        assert mocked_monzo._token == mocked_requests_post_json.return_value

        mocked_requests_post.assert_called_once_with(
            urljoin(mocked_monzo.api_url, '/oauth2/token'),
            data=expected_data,
        )
        mocked_requests_post_json.assert_called_once_with()
        mocked_save_token_on_disk.assert_called_once_with()

    def test_class_whoami_method(self, mocker, mocked_monzo):
        """Test class `whoami` method"""
        mocked_get_response = mocker.patch(
            'pymonzo.monzo_api.MonzoAPI._get_response',
        )

        result = mocked_monzo.whoami()

        mocked_get_response.assert_called_once_with(
            method='get', endpoint='/ping/whoami',
        )

        expected_result = mocked_get_response.return_value.json.return_value

        assert result == expected_result

    def test_class_accounts_method(self, mocker, mocked_monzo, accounts_api_response):
        """Test class `accounts` method"""
        mocked_get_response = mocker.patch(
            'pymonzo.monzo_api.MonzoAPI._get_response',
        )
        mocked_get_response.return_value.json.return_value = accounts_api_response

        assert mocked_monzo._cached_accounts is None

        result = mocked_monzo.accounts()

        mocked_get_response.assert_called_once_with(
            method='get', endpoint='/accounts',
        )

        accounts_json = accounts_api_response['accounts']
        expected_result = [
            MonzoAccount(data=account) for account in accounts_json
        ]

        assert result == expected_result
        assert mocked_monzo._cached_accounts == expected_result

        # Calling it again should fetch '_cached_accounts'
        mocked_get_response = mocker.patch(
            'pymonzo.monzo_api.MonzoAPI._get_response',
        )
        mocked_get_response.return_value.json.return_value = accounts_api_response

        result = mocked_monzo.accounts()

        assert mocked_get_response.call_count == 0

        assert result == mocked_monzo._cached_accounts

        # But calling it with 'refresh=True' should do an API request
        mocked_get_response = mocker.patch(
            'pymonzo.monzo_api.MonzoAPI._get_response',
        )
        mocked_get_response.return_value.json.return_value = accounts_api_response

        assert mocked_monzo._cached_accounts is not None

        result = mocked_monzo.accounts(refresh=True)

        mocked_get_response.assert_called_once_with(
            method='get', endpoint='/accounts',
        )

        accounts_json = accounts_api_response['accounts']
        expected_result = [
            MonzoAccount(data=account) for account in accounts_json
        ]

        assert result == expected_result
        assert mocked_monzo._cached_accounts == expected_result

    def test_class_balance_method(self, mocker, mocked_monzo,
                                  balance_api_response, accounts_api_response):
        """Test class `balance` method"""
        mocked_get_response = mocker.patch(
            'pymonzo.monzo_api.MonzoAPI._get_response',
        )
        mocked_get_response.return_value.json.return_value = balance_api_response

        accounts_json = accounts_api_response['accounts']
        mocked_monzo._cached_accounts = [
            MonzoAccount(data=account) for account in accounts_json
        ]

        result = mocked_monzo.balance()

        mocked_get_response.assert_called_once_with(
            method='get',
            endpoint='/balance',
            params={
                'account_id': mocked_monzo._cached_accounts[0].id,
            },
        )

        expected_result = MonzoBalance(balance_api_response)

        assert result == expected_result

        # It should raise an 'ValueError' if there more (or less) then 1 account
        mocked_monzo._cached_accounts = mocked_monzo._cached_accounts * 2

        with pytest.raises(ValueError):
            mocked_monzo.balance()

    def test_class_pots_method(self, mocker, mocked_monzo, pots_api_response):
        """Test class `pots` method"""
        mocked_get_response = mocker.patch(
            'pymonzo.monzo_api.MonzoAPI._get_response',
        )
        mocked_get_response.return_value.json.return_value = pots_api_response

        assert mocked_monzo._cached_pots is None

        result = mocked_monzo.pots()

        mocked_get_response.assert_called_once_with(
            method='get', endpoint='/pots/listV1',
        )

        pots_json = pots_api_response['pots']
        expected_result = [MonzoPot(data=pot) for pot in pots_json]

        assert result == expected_result
        assert mocked_monzo._cached_pots == expected_result

        # Calling it again should fetch '_cached_pots'
        mocked_get_response = mocker.patch(
            'pymonzo.monzo_api.MonzoAPI._get_response',
        )
        mocked_get_response.return_value.json.return_value = pots_api_response

        result = mocked_monzo.pots()

        assert mocked_get_response.call_count == 0

        assert result == mocked_monzo._cached_pots

        # But calling it with 'refresh=True' should do an API request
        mocked_get_response = mocker.patch(
            'pymonzo.monzo_api.MonzoAPI._get_response',
        )
        mocked_get_response.return_value.json.return_value = pots_api_response

        assert mocked_monzo._cached_pots is not None

        result = mocked_monzo.pots(refresh=True)

        mocked_get_response.assert_called_once_with(
            method='get', endpoint='/pots/listV1',
        )

        pots_json = pots_api_response['pots']
        expected_result = [MonzoPot(data=pot) for pot in pots_json]

        assert result == expected_result
        assert mocked_monzo._cached_pots == expected_result

    def test_class_transactions_method(self, mocker, mocked_monzo,
                                       transactions_api_response, accounts_api_response):
        """Test class `transactions` method"""
        mocked_get_response = mocker.patch(
            'pymonzo.monzo_api.MonzoAPI._get_response',
        )
        mocked_get_response.return_value.json.return_value = transactions_api_response

        accounts_json = accounts_api_response['accounts']
        mocked_monzo._cached_accounts = [
            MonzoAccount(data=account) for account in accounts_json
        ]

        result = mocked_monzo.transactions()

        mocked_get_response.assert_called_once_with(
            method='get',
            endpoint='/transactions',
            params={
                'account_id': mocked_monzo._cached_accounts[0].id,
            },
        )

        transactions_json = transactions_api_response['transactions']
        expected_result = [
            MonzoTransaction(data=transaction) for transaction in transactions_json
        ]

        assert result == expected_result

        # It should raise an 'ValueError' if there more (or less) then 1 account
        mocked_monzo._cached_accounts = mocked_monzo._cached_accounts * 2

        with pytest.raises(ValueError):
            mocked_monzo.transactions()

    def test_class_transaction_method(self, mocker, mocked_monzo, transaction_api_response):
        """Test class `transaction` method"""
        mocked_get_response = mocker.patch(
            'pymonzo.monzo_api.MonzoAPI._get_response',
        )
        mocked_get_response.return_value.json.return_value = transaction_api_response

        result = mocked_monzo.transaction('foobar')

        mocked_get_response.assert_called_once_with(
            method='get',
            endpoint='/transactions/foobar',
            params={},
        )

        expected_result = MonzoTransaction(transaction_api_response['transaction'])

        assert result == expected_result

        # With expanded merchant info
        mocked_get_response = mocker.patch(
            'pymonzo.monzo_api.MonzoAPI._get_response',
        )
        mocked_get_response.return_value.json.return_value = transaction_api_response

        result = mocked_monzo.transaction('foobar', expand_merchant=True)

        mocked_get_response.assert_called_once_with(
            method='get',
            endpoint='/transactions/foobar',
            params={
                'expand[]': 'merchant',
            },
        )

        expected_result = MonzoTransaction(transaction_api_response['transaction'])

        assert result == expected_result