## $Id: test_paypal.py 12498 2015-01-20 06:53:55Z uli $
##
## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program; if not, write to the Free Software
## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
##
import grok
import decimal
import os
import re
import shutil
import tempfile
import unittest
import paypalrestsdk
from zope.component import (
    getGlobalSiteManager, queryUtility,
    )
from zope.component.hooks import setSite, clearSite
from zope.i18nmessageid.message import Message as i18nMessage
from zope.interface import implements
from zope.interface.verify import verifyObject, verifyClass
from zope.schema.interfaces import IVocabularyTokenized, ITerm
from waeup.ikoba.interfaces import IPayPalConfig
from waeup.ikoba.payments.interfaces import (
    IPaymentGatewayService, IPayer, IPayee, IPaymentItem,
    )
from waeup.ikoba.payments.paypal import (
    get_paypal_config_file_path, parse_paypal_config, get_access_token,
    configure_sdk, get_payment, Payer, PayerInfo, ShippingAddress,
    ADDRESS_TYPES_VOCAB, IShippingAddress, Address, IAddress, to_dict,
    CreditCard, CREDIT_CARD_TYPES_VOCAB, CREDIT_CARD_STATUS_VOCAB,
    ICreditCard, ICreditCardToken, CreditCardToken, FundingInstrument,
    AmountDetails, IAmount, Amount, IItem, Item, STOCK_KEEPING_UNITS_VOCAB,
    IItemList, ItemList, IPaymentOptions, PaymentOptions, ITransaction,
    Transaction, PAYMENT_OPTION_METHODS_VOCAB, PayPalCreditCardService,
    PayPalRegularPaymentService,
    )
from waeup.ikoba.testing import (
    FunctionalLayer, FunctionalTestCase,
    )


#
# PayPal test config
#

EXTERNAL_PAYPAL_TESTS = False

#
# End of PayPal test config
#


def external_paypal_test(func):
    """A decorator that can block test functions.
    """
    if not EXTERNAL_PAYPAL_TESTS:
        myself = __file__
        if myself.endswith('.pyc'):
            myself = myself[:-2]
        print "WARNING: external paypal tests are skipped!"
        print "WARNING: edit %s to enable them." % myself
        return
    return func


class HelperTests(unittest.TestCase):

    def setUp(self):
        self.workdir = tempfile.mkdtemp()

    def tearDown(self):
        shutil.rmtree(self.workdir)
        # unregister any remaining utils registered during tests
        util = queryUtility(IPayPalConfig)
        if util is not None:
            getGlobalSiteManager().unregisterUtility(util, IPayPalConfig)

    def test_get_paypal_config_file_path(self):
        # we can get a config file path (if registered)
        path = os.path.join(self.workdir, 'sample.cfg')
        getGlobalSiteManager().registerUtility(
            {'path': path, }, IPayPalConfig, '')
        result = get_paypal_config_file_path()
        assert result == path

    def test_get_paypal_config_file_path_no_file(self):
        # without IPayPalConfig util set, we get `None`
        result = get_paypal_config_file_path()
        assert result is None

    def test_parse_paypal_config(self):
        # we can parse paypal configs
        path = os.path.join(self.workdir, 'sample.cfg')
        open(path, 'w').write(
            "[rest-client]\n"
            "id = myid\n"
            "secret = mysecret\n"
            "mode = live\n")
        result = parse_paypal_config(path)
        assert result['client_id'] == u'myid'
        assert result['client_secret'] == u'mysecret'
        assert result['mode'] == 'live'

    def test_parse_paypal_config_defaults(self):
        # we have fallback values (defaults) in place
        path = os.path.join(self.workdir, 'sample.cfg')
        open(path, 'w').write(
            "[rest-client]\n"
            )
        result = parse_paypal_config(path)
        assert result['client_id'] is None
        assert result['client_secret'] is None
        assert result['mode'] == "sandbox"

    def test_configure_sdk(self):
        # we can configure the paypal sdk
        path = os.path.join(self.workdir, 'sample.cfg')
        open(path, 'w').write(
            "[rest-client]\n"
            "id = my-special-id\n"
            )
        result = configure_sdk(path)
        assert result['client_id'] == 'my-special-id'
        assert result['client_secret'] is None
        assert result['mode'] == "sandbox"
        assert paypalrestsdk.api.__api__.mode == 'sandbox'
        assert paypalrestsdk.api.__api__.client_id == 'my-special-id'

    def test_get_payment_invalid_intent(self):
        # only 'sale' is currently allowed
        self.assertRaises(
            ValueError, get_payment, intent='invalid')
        self.assertRaises(
            ValueError, get_payment, intent='order')

    def test_to_dict(self):
        # we can turn objects into dicts
        class C1(object):
            a = 1
            b = "somestring"
            c = u"unicodestring"
            d = None
        obj = C1()
        self.assertEqual(
            to_dict(obj),
            {'a': u"1",
             'b': u"somestring",
             'c': u"unicodestring",
             }
            )

    def test_to_dict_map(self):
        # we can map attribute names
        class C1(object):
            a = 1
            b = "somestring"
        obj = C1()
        self.assertEqual(
            to_dict(obj, name_map={'a': 'replaced_a'}),
            {'replaced_a': u"1",
             'b': u"somestring",
             })

    def test_to_dict_lists(self):
        # to_dict can handle lists
        class C1(object):
            a = 1

            def to_dict(self):
                return to_dict(self)

        obj1 = C1()
        obj2 = C1()
        obj2.a = 2
        obj3 = C1()
        obj3.a = 3
        obj1.foo = [obj2, obj3]
        self.assertEqual(
            to_dict(obj1),
            {
                'a': u'1',
                'foo': [
                    {'a': u'2'},
                    {'a': u'3'},
                    ]
                }
            )

    def test_to_dict_decimals(self):
        # decimals are converted to numbers with 2 decimals
        class C1(object):
            a = decimal.Decimal("0.1")
        self.assertEqual(
            to_dict(C1()),
            {
                'a': u"0.10",
                }
            )


class PayerTests(unittest.TestCase):

    def test_create(self):
        # we can create payer objects
        payer = Payer(payment_method='paypal')
        assert payer.payment_method == 'paypal'
        assert payer.funding_instruments == []
        assert payer.payer_info is None
        assert payer.status is None

    def test_init_invalid_payment_meth(self):
        # we must provide a valid payment method
        payer = Payer(payment_method='paypal')
        assert payer is not None
        payer = Payer(payment_method='credit_card')
        assert payer is not None
        self.assertRaises(
            ValueError, Payer, payment_method='invalid')

    def test_init_invalid_payer_state(self):
        # only certain values are allowed as payer states
        payer = Payer(payment_method='paypal', status='VERIFIED')
        assert payer is not None
        payer = Payer(payment_method='paypal', status='UNVERIFIED')
        assert payer is not None
        self.assertRaises(
            ValueError, Payer, payment_method='paypal', status='InVaLiD')


class PayerInfoTests(unittest.TestCase):

    def test_create(self):
        # we can create payer infos
        info = PayerInfo()
        assert info.email == ''
        assert info.first_name == ''
        assert info.last_name == ''
        assert info.payer_id == ''
        assert info.phone == ''
        assert info.shipping_address is None
        assert info.tax_id_type == ''
        assert info.tax_id == ''

    def test_init_invalid_tax_id_type(self):
        # onyl certain tax id types are allowed
        info = PayerInfo(tax_id_type='BR_CPF')
        assert info is not None
        info = PayerInfo(tax_id_type='BR_CNPJ')
        assert info is not None
        self.assertRaises(
            ValueError, PayerInfo, tax_id_type='INVALID_TYPE')


class CreditCardTests(unittest.TestCase):

    def test_iface(self):
        # we fullfill any interface contracts
        credit_card = CreditCard(
            number=u"12345678",
            credit_card_type="visa",
            expire_month=4,
            expire_year=2012,
            )
        verifyClass(ICreditCard, CreditCard)
        verifyObject(ICreditCard, credit_card)

    def test_create(self):
        # we can create CreditCard objects
        credit_card = CreditCard(
            number=u"12345678",
            credit_card_type="visa",
            expire_month=4,
            expire_year=2012,
            )
        assert credit_card.paypal_id is None
        assert credit_card.external_customer_id is not None
        assert credit_card.number == u"12345678"
        assert credit_card.credit_card_type == "visa"
        assert credit_card.expire_month == 4
        assert credit_card.expire_year == 2012
        assert credit_card.cvv2 is None
        assert credit_card.first_name is None
        assert credit_card.last_name is None
        assert credit_card.billing_address is None
        assert credit_card.state is None
        assert credit_card.paypal_valid_until is None

    def test_external_customer_id_given(self):
        # we do not override given curstomer ids
        credit_card = CreditCard(
            number=u"12345678",
            credit_card_type="visa",
            expire_month=4,
            expire_year=2012,
            external_customer_id=u'MySpecialPayerId',
            )
        assert credit_card.external_customer_id == u'MySpecialPayerId'

    def test_external_customer_id_not_given(self):
        # in case no customer id is given, we generate one
        credit_card = CreditCard(
            number=u"12345678",
            credit_card_type="visa",
            expire_month=4,
            expire_year=2012,
            )
        # our customer ids contain a leading 'PAYER_' and 32 hex digits
        assert re.match(
            'PAYER_[0-9a-f]{32}$', credit_card.external_customer_id)

    def test_number_is_checked(self):
        # we do not accept invalid numbers
        self.assertRaises(
            ValueError, CreditCard,
            number=u"not-a-number",
            credit_card_type="visa",
            expire_month=4,
            expire_year=2012,
            )

    def test_to_str(self):
        # we can turn CreditCard objects into dicts.
        addr = Address(
            line1=u"52 N Main ST",
            city=u"Johnstown",
            state=u"OH",
            postal_code=u"43210",
            country_code=u"US")
        credit_card = CreditCard(
            credit_card_type=u"visa",
            external_customer_id=u"PAYER_0123456789012345678901",
            number=u"4417119669820331",
            expire_month=11,
            expire_year=2018,
            cvv2=u"874",
            first_name=u"Joe",
            last_name=u"Shopper",
            billing_address=addr)
        self.assertEqual(
            credit_card.to_dict(),
            {
                "type": u"visa",
                "number": u"4417119669820331",
                "external_customer_id": u"PAYER_0123456789012345678901",
                "expire_month": u"11",
                "expire_year": u"2018",
                "cvv2": u"874",
                "first_name": u"Joe",
                "last_name": u"Shopper",
                "billing_address": {
                    "line1": u"52 N Main ST",
                    "city": u"Johnstown",
                    "state": u"OH",
                    "postal_code": u"43210",
                    "country_code": u"US"}
                }
            )


class CreditCardTokenTests(unittest.TestCase):

    def test_iface(self):
        # we fullfill any interface contracts
        token = CreditCardToken(
            credit_card_id=u"12345678",
            )
        verifyClass(ICreditCardToken, CreditCardToken)
        verifyObject(ICreditCardToken, token)

    def test_create(self):
        # we can create CreditCardToken objects
        token = CreditCardToken(
            credit_card_id=u"12345678",
            )
        assert token.credit_card_id == u"12345678"
        assert token.external_customer_id is None
        assert token.credit_card_type is None
        assert token.expire_month is None
        assert token.expire_year is None
        assert token.last4 is None

    def test_customer_id_given(self):
        # we do not override given customer ids
        token = CreditCardToken(
            credit_card_id=u"12345678",
            external_customer_id=u'MySpecialPayerId',
            )
        assert token.external_customer_id == u'MySpecialPayerId'

    def test_to_str(self):
        # we can turn CreditCardToken objects into dicts.
        token = CreditCardToken(
            credit_card_type=u"visa",
            external_customer_id=u"PAYER_0123456789012345678901",
            credit_card_id=u"12345678",
            last4="8901",
            expire_month=11,
            expire_year=2018,
            )
        self.assertEqual(
            token.to_dict(),
            {
                "credit_card_id": u"12345678",
                "external_customer_id": u"PAYER_0123456789012345678901",
                "last4": u"8901",
                "type": u"visa",
                "expire_month": u"11",
                "expire_year": u"2018",
                }
            )


class FundingInstrumentTests(unittest.TestCase):

    def test_create(self):
        # we can create FundingInstrument objects
        token = CreditCardToken(
            credit_card_id=u"12345678",
            )
        instr = FundingInstrument(credit_card_token=token)
        assert instr.credit_card_token is token

    def test_require_credit_card_or_token(self):
        # we require a credit card object or a token.
        credit_card = CreditCard(
            number=u"12345678",
            credit_card_type="visa",
            expire_month=4,
            expire_year=2012,
            )
        token = CreditCardToken(
            credit_card_id=u"12345678",
            )
        self.assertRaises(
            ValueError, FundingInstrument,
            credit_card=credit_card,
            credit_card_token=token
            )
        self.assertRaises(
            ValueError, FundingInstrument
            )

    def test_to_dict(self):
        # we can turn Funding instruments into dicts
        token = CreditCardToken(
            credit_card_type=u"visa",
            external_customer_id=u"PAYER_0123456789012345678901",
            credit_card_id=u"12345678",
            last4="8901",
            expire_month=11,
            expire_year=2018,
            )
        instr = FundingInstrument(credit_card_token=token)
        result = instr.to_dict()
        self.assertEqual(
            result,
            {
                "credit_card_token": {
                    "credit_card_id": u"12345678",
                    "external_customer_id": u"PAYER_0123456789012345678901",
                    "last4": u"8901",
                    "type": u"visa",
                    "expire_month": u"11",
                    "expire_year": u"2018",
                    }
                }
            )


class AddressTests(unittest.TestCase):

    def test_iface(self):
        # we fullfill any interface contracts
        addr = Address(
            line1=u'Address Line 1',
            city=u'Somecity',
            country_code=u'AT',
            )
        verifyClass(IAddress, Address)
        verifyObject(IAddress, addr)

    def test_create(self):
        # we can create addresses
        addr = Address(
            line1=u'Honey Street 1',
            city=u'Beartown',
            country_code=u'GB',
            )
        assert addr.line1 == u'Honey Street 1'
        assert addr.line2 is None
        assert addr.city == u'Beartown'
        assert addr.country_code == u'GB'
        assert addr.postal_code is None
        assert addr.state is None
        assert addr.phone is None

    def test_to_dict(self):
        # we can turn addresses into dicts
        addr = Address(
            line1=u'Honey Street 1',
            city=u'Beartown',
            country_code=u'GB',
            )
        self.assertEqual(
            addr.to_dict(),
            {
                'line1': u'Honey Street 1',
                'city': u'Beartown',
                'country_code': u'GB',
                }
            )
        addr.line2 = u"Behind little tree"
        self.assertEqual(
            addr.to_dict(),
            {
                'line1': u'Honey Street 1',
                'line2': u'Behind little tree',
                'city': u'Beartown',
                'country_code': u'GB',
                }
            )


class ShippingAddressTests(unittest.TestCase):

    def test_iface(self):
        # we fullfill any interface contracts
        addr = ShippingAddress(
            recipient_name=u'Foo Bar',
            line1=u'Address Line 1',
            city=u'Somecity',
            country_code=u'AT',
            )
        verifyClass(IShippingAddress, ShippingAddress)
        verifyObject(IShippingAddress, addr)

    def test_create(self):
        # we can create shipping addresses
        addr = ShippingAddress(
            recipient_name=u'Rob Receiver',
            line1=u'Honey Street 1',
            city=u'Beartown',
            country_code=u'GB',
            )
        assert addr.recipient_name == u'Rob Receiver'
        assert addr.type == u'residential'
        assert addr.line1 == u'Honey Street 1'
        assert addr.line2 is None
        assert addr.city == u'Beartown'
        assert addr.country_code == u'GB'
        assert addr.postal_code is None
        assert addr.state is None
        assert addr.phone is None

    def test_to_dict(self):
        # we can turn shipping addresses into dicts
        addr = ShippingAddress(
            recipient_name=u'Rob Receiver',
            line1=u'Honey Street 1',
            city=u'Beartown',
            country_code=u'GB',
            )
        self.assertEqual(
            addr.to_dict(),
            {
                'recipient_name': u'Rob Receiver',
                'type': u'residential',
                'line1': u'Honey Street 1',
                'city': u'Beartown',
                'country_code': u'GB',
                }
            )
        addr.line2 = u"Behind little tree"
        self.assertEqual(
            addr.to_dict(),
            {
                'recipient_name': u'Rob Receiver',
                'type': u'residential',
                'line1': u'Honey Street 1',
                'line2': u'Behind little tree',
                'city': u'Beartown',
                'country_code': u'GB',
                }
            )


class AmountDetailsTests(unittest.TestCase):

    def test_create(self):
        # we can create AmountDetail objects
        details = AmountDetails()
        assert details.shipping is None
        assert details.subtotal is None
        assert details.tax is None
        assert details.fee is None
        assert details.handling_fee is None
        assert details.insurance is None
        assert details.shipping_discount is None

    def test_to_dict(self):
        # we can turn AmountDetails into a dict
        details = AmountDetails(
            shipping=decimal.Decimal("0.10"),
            tax=decimal.Decimal("0.30"),
            fee=decimal.Decimal("0.40"),
            handling_fee=decimal.Decimal("0.50"),
            insurance=decimal.Decimal("0.60"),
            shipping_discount=decimal.Decimal("0.70")
            )
        self.assertEqual(
            details.to_dict(),
            {
                'shipping': u"0.10",
                'subtotal': u"1.20",
                'tax': u"0.30",
                'fee': u"0.40",
                'handling_fee': u"0.50",
                'insurance': u"0.60",
                'shipping_discount': u"0.70"
                }
            )

    def test_subtotal_all_none(self):
        # if all items are none, also subtotal is none
        details = AmountDetails(
            shipping=None, tax=None, fee=None, handling_fee=None,
            insurance=None, shipping_discount=None,
            )
        assert details.subtotal is None
        details.shipping_discount = decimal.Decimal("1.00")
        assert details.subtotal == decimal.Decimal("-1.00")

    def test_subtotal_sum(self):
        # subtotal sums up correctly
        details = AmountDetails(
            shipping=decimal.Decimal("0.05"),
            tax=decimal.Decimal("0.40"),
            fee=decimal.Decimal("3.00"),
            handling_fee=decimal.Decimal("20.00"),
            insurance=decimal.Decimal("100.00"),
            shipping_discount=None
            )
        self.assertEqual(details.subtotal, decimal.Decimal("123.45"))
        details.shipping_discount = decimal.Decimal("0.00")
        self.assertEqual(details.subtotal, decimal.Decimal("123.45"))
        details.shipping_discount = decimal.Decimal("23.45")
        self.assertEqual(details.subtotal, decimal.Decimal("100.00"))


class AmountTests(unittest.TestCase):

    def test_iface(self):
        # we fullfill any interface contracts.
        amount = Amount()
        verifyClass(IAmount, Amount)
        verifyObject(IAmount, amount)

    def test_create(self):
        # we can create amount objects
        details = AmountDetails(
            shipping=decimal.Decimal("0.05"),
            tax=decimal.Decimal("0.40"),
            fee=decimal.Decimal("3.00"),
            handling_fee=decimal.Decimal("20.00"),
            insurance=decimal.Decimal("100.00"),
            shipping_discount=None
            )
        amount = Amount(
            total=decimal.Decimal("12.12"),
            currency="USD",
            details=details
            )
        assert amount.total == decimal.Decimal("12.12")
        assert amount.currency == "USD"
        assert amount.details is details

    def test_to_dict(self):
        # we can turn Amount objects into dicts
        self.maxDiff = None
        details = AmountDetails(
            shipping=decimal.Decimal("0.05"),
            tax=decimal.Decimal("0.40"),
            fee=decimal.Decimal("3.00"),
            handling_fee=decimal.Decimal("20.00"),
            insurance=decimal.Decimal("100.00"),
            shipping_discount=None
            )
        amount = Amount(
            total=decimal.Decimal("12.12"),
            currency="USD",
            details=details
            )
        self.assertEqual(
            amount.to_dict(),
            {
                'total': u'12.12',
                'currency': u'USD',
                'details': {
                    'shipping': u'0.05',
                    'subtotal': u'123.45',
                    'tax': u'0.40',
                    'fee': u'3.00',
                    'handling_fee': u'20.00',
                    'insurance': u'100.00',
                    }
                }
            )


class ItemTests(unittest.TestCase):

    def test_iface(self):
        # we fullfill all interface contracts
        item = Item(name=u"Splendid Item")
        verifyClass(IItem, Item)
        verifyObject(IItem, item)

    def test_create(self):
        # we can create Item objects
        item = Item(
            quantity=3,
            name=u"Splendid Thing",
            price=decimal.Decimal("1.1"),
            currency="USD",
            sku="pcs",
            description=u"Soo splendid!",
            tax=decimal.Decimal("0.1"),
            )
        assert item.quantity == 3
        assert item.name == u"Splendid Thing"
        assert item.price == decimal.Decimal("1.1")
        assert item.currency == "USD"
        assert item.sku == "pcs"
        assert item.description == u"Soo splendid!"
        assert item.tax == decimal.Decimal("0.1")

    def test_to_dict(self):
        # we can turn Item objects into dicts
        item = Item(
            quantity=3,
            name=u"Splendid Thing",
            price=decimal.Decimal("1.1"),
            currency="USD",
            sku="pcs",
            description=u"Soo splendid!",
            tax=decimal.Decimal("0.1"),
            )
        self.assertEqual(
            item.to_dict(),
            {
                "quantity": u"3",
                "name": u"Splendid Thing",
                "price": u"1.10",
                "currency": u"USD",
                "sku": u"pcs",
                "description": u"Soo splendid!",
                "tax": u"0.10"
                }
            )


class ItemListTests(unittest.TestCase):

    def test_iface(self):
        # we fullfill all interface contracts
        item_list = ItemList()
        verifyClass(IItemList, ItemList)
        verifyObject(IItemList, item_list)

    def test_create_minimal(self):
        # we can create ItemLists with a minimum of params
        item_list = ItemList()
        assert item_list.shipping_address is None
        assert item_list.items == []

    def test_create(self):
        # we can create ItemLists
        item1 = Item(
            name=u"Splendid Thing",
            )
        item2 = Item(
            name=u"Other Splendid Thing",
            )
        addr = ShippingAddress(
            recipient_name=u'Rob Receiver',
            line1=u'Honey Street 1',
            city=u'Beartown',
            country_code=u'GB',
            )
        item_list = ItemList(
            shipping_address=addr,
            items=[item1, item2])
        assert item_list.shipping_address is addr
        assert item_list.items == [item1, item2]

    def test_to_dict(self):
        # we can turn ITemLists into dicts
        item = Item(
            quantity=3,
            name=u"Splendid Thing",
            price=decimal.Decimal("1.1"),
            currency="USD",
            sku="pcs",
            description=u"Soo splendid!",
            tax=decimal.Decimal("0.1"),
            )
        addr = ShippingAddress(
            recipient_name=u'Rob Receiver',
            line1=u'Honey Street 1',
            city=u'Beartown',
            country_code=u'GB',
            )
        item_list = ItemList(items=[item, ], shipping_address=addr)
        self.assertEqual(
            item_list.to_dict(),
            {
                "items": [
                    {
                        "quantity": u"3",
                        "name": u"Splendid Thing",
                        "price": u"1.10",
                        "currency": u"USD",
                        "sku": u"pcs",
                        "description": u"Soo splendid!",
                        "tax": u"0.10"
                        }
                    ],
                "shipping_address":
                {
                    'recipient_name': u'Rob Receiver',
                    'type': u'residential',
                    'line1': u'Honey Street 1',
                    'city': u'Beartown',
                    'country_code': u'GB',
                    }
                }
            )


class PaymentOptionsTests(unittest.TestCase):

    def test_iface(self):
        # we fullfill all interface contracts
        opts = PaymentOptions()
        verifyClass(IPaymentOptions, PaymentOptions)
        verifyObject(IPaymentOptions, opts)

    def test_create(self):
        # we can create PaymentOptions objects
        opts = PaymentOptions()
        assert opts.allowed_payment_method is None

    def test_allowed_payment_method_checked_in_init(self):
        # any value apart from None, INSTANT... is rejected in __init__
        self.assertRaises(
            ValueError,
            PaymentOptions, allowed_payment_method='NoTvAlID')

    def test_to_dict(self):
        # we can turn PaymentOptions into dicts
        opts = PaymentOptions(
            allowed_payment_method="INSTANT_FUNDING_SOURCE")
        self.assertEqual(
            opts.to_dict(),
            {
                'allowed_payment_method': "INSTANT_FUNDING_SOURCE",
                }
            )


class TransactionTests(unittest.TestCase):

    def test_iface(self):
        # we fullfill all interface contracts
        amount = Amount()
        transaction = Transaction(amount=amount)
        verifyClass(ITransaction, Transaction)
        verifyObject(ITransaction, transaction)

    def test_create(self):
        # we can create transacions
        amount = Amount()
        transaction = Transaction(amount=amount)
        assert transaction.amount is amount
        assert transaction.description is None
        assert transaction.item_list is None
        assert transaction.related_resources == []
        assert transaction.invoice_number is None
        assert transaction.custom is None
        assert transaction.soft_descriptor is None
        assert transaction.payment_options is None

    def test_to_dict(self):
        # we can turn Transaction objects into dicts
        transaction = Transaction(
            amount=Amount(),
            description=u"My description",
            item_list=ItemList(),
            related_resources=[],
            invoice_number=u"12345",
            custom=u"Some custom remark",
            soft_descriptor=u"softdescriptor?",
            payment_options=PaymentOptions(
                allowed_payment_method="INSTANT_FUNDING_SOURCE"),
            )
        self.assertEqual(
            transaction.to_dict(), {
                'amount': {'currency': u'USD', 'total': u'0.00'},
                'custom': u'Some custom remark',
                'description': u'My description',
                'invoice_number': u'12345',
                'item_list': {
                    'items': []
                    },
                'payment_options': {
                    'allowed_payment_method': u'INSTANT_FUNDING_SOURCE'
                    },
                'related_resources': [],
                'soft_descriptor': u'softdescriptor?'
                }
            )


class AddressTypesVocabTests(unittest.TestCase):

    def test_address_types_vocab_tokenized(self):
        # we can get a countries source suitable for forms etc.
        verifyObject(IVocabularyTokenized, ADDRESS_TYPES_VOCAB)

    def test_address_types_vocab_i18nized(self):
        # vocab titles are i18nized
        result = ADDRESS_TYPES_VOCAB.getTerm('residential')
        assert ITerm.providedBy(result)
        self.assertEqual(result.title, u'residential')
        assert isinstance(result.title, i18nMessage)

    def test_address_types_vocab_tokens_are_string(self):
        # vocab tokens are simple strings
        result = ADDRESS_TYPES_VOCAB.getTerm('residential')
        assert ITerm.providedBy(result)
        assert result.token == result.value
        assert result.value == 'residential'
        assert isinstance(result.token, str)
        assert isinstance(result.value, str)


class CreditCardTypesVocabTests(unittest.TestCase):

    def test_credit_card_types_vocab_tokenized(self):
        # we can get a countries source suitable for forms etc.
        verifyObject(IVocabularyTokenized, CREDIT_CARD_TYPES_VOCAB)

    def test_credit_cards_types_vocab_i18nized(self):
        # vocab titles are i18nized
        result = CREDIT_CARD_TYPES_VOCAB.getTerm('visa')
        assert ITerm.providedBy(result)
        self.assertEqual(result.title, u'visa')
        assert isinstance(result.title, i18nMessage)

    def test_credit_cards_types_vocab_tokens_are_string(self):
        # vocab tokens are simple strings
        result = CREDIT_CARD_TYPES_VOCAB.getTerm('visa')
        assert ITerm.providedBy(result)
        assert result.token == result.value
        assert result.value == 'visa'
        assert isinstance(result.token, str)
        assert isinstance(result.value, str)


class CreditcardstatusVocabTests(unittest.TestCase):

    def test_credit_card_status_vocab_tokenized(self):
        # we can get a countries source suitable for forms etc.
        verifyObject(IVocabularyTokenized, CREDIT_CARD_STATUS_VOCAB)

    def test_credit_cards_status_vocab_i18nized(self):
        # vocab titles are i18nized
        result = CREDIT_CARD_STATUS_VOCAB.getTerm('ok')
        assert ITerm.providedBy(result)
        self.assertEqual(result.title, u'ok')
        assert isinstance(result.title, i18nMessage)

    def test_credit_cards_status_vocab_tokens_are_string(self):
        # vocab tokens are simple strings
        result = CREDIT_CARD_STATUS_VOCAB.getTerm('expired')
        assert ITerm.providedBy(result)
        assert result.token == result.value
        assert result.value == 'expired'
        assert isinstance(result.token, str)
        assert isinstance(result.value, str)


class StockKeepingUnitsVocabTests(unittest.TestCase):

    def test_sku_vocab_tokenized(self):
        # we can get a tokenzed vocab for stock keeping units
        verifyObject(IVocabularyTokenized, STOCK_KEEPING_UNITS_VOCAB)

    def test_sku_vocab_i18nized(self):
        # vocab titles are i18nized
        result = STOCK_KEEPING_UNITS_VOCAB.getTerm('pcs')
        assert ITerm.providedBy(result)
        self.assertEqual(result.title, u'pieces')
        assert isinstance(result.title, i18nMessage)

    def test_sku_vocab_tokens_are_string(self):
        # vocab tokens are simple strings
        result = STOCK_KEEPING_UNITS_VOCAB.getTerm('pcs')
        assert ITerm.providedBy(result)
        assert result.token == result.value
        assert result.value == 'pcs'
        assert isinstance(result.token, str)
        assert isinstance(result.value, str)


class PaymentOptionMethodsVocabTests(unittest.TestCase):

    def test_payment_option_methods_vocab_tokenized(self):
        # we can get a countries source suitable for forms etc.
        verifyObject(IVocabularyTokenized, PAYMENT_OPTION_METHODS_VOCAB)

    def test_payment_option_methods_vocab_i18nized(self):
        # vocab titles are i18nized
        result = PAYMENT_OPTION_METHODS_VOCAB.getTerm('INSTANT_FUNDING_SOURCE')
        assert ITerm.providedBy(result)
        self.assertEqual(result.title, u'INSTANT_FUNDING_SOURCE')
        assert isinstance(result.title, i18nMessage)

    def test_payment_option_methods_vocab_tokens_are_string(self):
        # vocab tokens are simple strings
        result = PAYMENT_OPTION_METHODS_VOCAB.getTerm('INSTANT_FUNDING_SOURCE')
        assert ITerm.providedBy(result)
        assert result.token == result.value
        assert result.value == 'INSTANT_FUNDING_SOURCE'
        assert isinstance(result.token, str)
        assert isinstance(result.value, str)


class FakePayer(object):

    implements(IPayer)

    payer_id = 'PAYER-123'
    first_name = u'Joe'
    last_name = u'Shopper'


class FakePayee(object):

    implements(IPayee)

    payee_id = 'PAYEE-456'


class FakePaymentItem(object):

    implements(IPaymentItem)

    title = u'Fake Item'
    item_id = 'BILL-123456'
    amount = decimal.Decimal("12.10")
    currency = 'EUR'


# Make sure all fake classes (and objects made of them) are up-to-date
verifyClass(IPayer, FakePayer)
verifyObject(IPayer, FakePayer())
verifyClass(IPayee, FakePayee)
verifyObject(IPayee, FakePayee())
verifyClass(IPaymentItem, FakePaymentItem)
verifyObject(IPaymentItem, FakePaymentItem())


class PayPalCreditCardServiceTests(FunctionalTestCase):

    layer = FunctionalLayer

    def tearDown(self):
        super(PayPalCreditCardServiceTests, self).tearDown()
        clearSite()

    def create_site(self):
        # create a simple site in root.
        class MyApp(grok.Application, grok.Container):
            pass
        site = MyApp()
        self.getRootFolder()['app'] = site
        setSite(site)
        return site

    def get_credit_card(self):
        # get a valid credit card
        credit_card = CreditCard(
            credit_card_type=u"visa",
            external_customer_id=u"PAYER_0123456789012345678901",
            number=u"4417119669820331",
            expire_month=11,
            expire_year=2018,
            cvv2=u"874",
            first_name=u"Joe",
            last_name=u"Shopper",
            )
        return credit_card

    def test_iface(self):
        # we fullfill all interface contracts
        service = PayPalCreditCardService()
        verifyClass(IPaymentGatewayService, PayPalCreditCardService)
        verifyObject(IPaymentGatewayService, service)

    def test_get_credit_card_no_site(self):
        # we get simply None if no site is registered
        service = PayPalCreditCardService()
        assert service.get_credit_card('not-a-valid-payer-id') is None

    def test_get_credit_card_site_not_a_container(self):
        # we get simply None if site is not a container
        site = grok.Application()  # does not provide IContainer
        self.getRootFolder()['app'] = site
        setSite(site)
        service = PayPalCreditCardService()
        assert service.get_credit_card('not-a-valid-payer-id') is None

    def test_get_credit_card_no_container(self):
        # we get simply None if no 'creditcards' container is in site
        self.create_site()
        service = PayPalCreditCardService()
        assert service.get_credit_card('not-a-valid-payer-id') is None

    def test_get_credit_card(self):
        # we can get a credit card, if one was stored
        class MyCard(object):
            myid = 'CARD1'

            def __eq__(self, obj):
                return self.myid == obj.myid

        site = self.create_site()
        site['creditcards'] = grok.Container()
        card = MyCard()
        card.myid = 'CHANGED CARD ID'
        site['creditcards'][u'CARD1'] = card
        service = PayPalCreditCardService()
        assert service.get_credit_card(u'CARD1') == card
        assert service.get_credit_card(u'CARD2') is None

    @external_paypal_test
    def test_store_credit_card(self):
        # we can (and must) store credit card data online.
        site = self.create_site()
        assert 'creditcards' not in site
        service = PayPalCreditCardService()
        credit_card = self.get_credit_card()
        result = service.store_credit_card(credit_card)
        # a missing creditcards container is created on-the-fly
        assert 'creditcards' in site
        assert ICreditCardToken.providedBy(result)
        assert result.external_customer_id in site['creditcards']
        assert site['creditcards'][result.external_customer_id] == result

    @external_paypal_test
    def test_store_credit_card_invalid(self):
        # an exception is raised with invalid credit cards.
        site = self.create_site()
        service = PayPalCreditCardService()
        credit_card = CreditCard(
            number=u"12345678",
            credit_card_type="visa",
            expire_month=4,
            expire_year=2012,
            )
        self.assertRaises(
            IOError, service.store_credit_card, credit_card)

    def test_create_payment_no_credictard(self):
        # trying to create a payment without credit card raises an exception
        service = PayPalCreditCardService()
        exception = None
        try:
            service.create_payment(
                payer=FakePayer(),
                payment_item=FakePaymentItem(),
                payee=FakePayee()
                )
        except (ValueError, ) as err:
            exception = err
        assert exception.message == 'Payer PAYER-123 has no credit card.'

    @external_paypal_test
    def test_create_payment(self):
        # we can actually create payments
        service = PayPalCreditCardService()
        site = self.create_site()
        credit_card = self.get_credit_card()
        result = service.store_credit_card(credit_card)
        payer = FakePayer()
        payer.payer_id = result.external_customer_id
        payment_item = FakePaymentItem()
        payment = service.create_payment(
            payer=payer, payment_item=payment_item)
        result = payment.create()
        assert result is True


class FunctionalPaypalTests(FunctionalTestCase):

    layer = FunctionalLayer

    @external_paypal_test
    def test_get_access_token(self):
        # we can get an access token
        result = get_access_token()
        assert isinstance(result, unicode)

    @external_paypal_test
    def test_get_payment(self):
        # we can construct (paypal-)payment objects
        payment = get_payment()
        assert isinstance(payment, paypalrestsdk.Payment)
        result = payment.create()
        assert result is True

    def test_paypal_services_registered(self):
        # the PayPal gateway services are all registered
        creditcard_service = queryUtility(
            IPaymentGatewayService, name=u'paypal_creditcard')
        assert creditcard_service is not None
        assert isinstance(creditcard_service, PayPalCreditCardService)
        paypal_regular_service = queryUtility(
            IPaymentGatewayService, name=u'paypal_regular')
        assert paypal_regular_service is not None
        assert isinstance(paypal_regular_service, PayPalRegularPaymentService)
