# -*- coding: utf-8 -*-
## $Id: test_payment.py 12775 2015-03-16 12:53:09Z 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 datetime
import decimal
import re
import unittest
from zope.component import (
    getUtilitiesFor, getSiteManager, queryUtility, getGlobalSiteManager,
    )
from zope.component.hooks import setSite
from zope.interface import implements, implementer
from zope.interface.verify import verifyClass, verifyObject
from waeup.ikoba.payments.interfaces import (
    IPayment, STATE_UNPAID, STATE_PAID, STATE_FAILED,
    IPaymentGatewayService, IPaymentItem, IPaymentGatewayServicesLister,
    IPayableFinder, IPayerFinder, IPayable, IPayer,
    )
from waeup.ikoba.app import Company
from waeup.ikoba.payments.payment import (
    Payment, get_payment_providers, PaymentItem, format_payment_item_values,
    get_payment, get_payments_from_payer_id, find_payable_from_payable_id,
    find_payer_from_payer_id, get_payments_from_payable_id, format_amount,
    )
from waeup.ikoba.testing import (FunctionalLayer, FunctionalTestCase)


@implementer(IPayer)
class FakePayer(object):

    def __init__(
        self, payer_id=u'PAYER_01', first_name=u'Anna', last_name='Tester'):
        self.payer_id = payer_id
        self.first_name = first_name
        self.last_name = last_name


FAKE_PAYMENT_ITEMS = (
    PaymentItem(u'ITEM1', u'Item title 1', decimal.Decimal("1.00")),
    PaymentItem(u'ITEM2', u'Item title 2', decimal.Decimal("2.2")),
    )


@implementer(IPayable)
class FakePayable(object):

    payable_id = u'id1'

    def __init__(self, payable_id=u'PAYABLE_01', title=u'title',
                 currency=u'USD', payment_items=FAKE_PAYMENT_ITEMS):
        self.payable_id = payable_id
        self.title = title
        self.currency = currency
        self.payment_items = payment_items


class HelperTests(unittest.TestCase):

    def tearDown(self):
        # unregister any IPaymentGatewayServices
        sm = getSiteManager(None)
        for name, util in getUtilitiesFor(IPaymentGatewayService):
            sm.unregisterUtility(util, name=name)

    def test_get_payment_providers_no_providers(self):
        # we can get a dict of all payment providers
        result = get_payment_providers()
        assert isinstance(result, dict)
        assert result == {}

    def test_get_payment_providers(self):
        # we get any payment providers registered
        sm = getSiteManager(None)

        class FakeUtil(object):
            implements(IPaymentGatewayService)

        fake_util = FakeUtil()
        sm.registerUtility(fake_util, name=u'some_name')
        result = get_payment_providers()
        assert isinstance(result, dict)
        assert result.keys() == ['some_name', ]
        assert result['some_name'] is fake_util

    def test_format_payment_item_values(self):
        # we can format lists of payment item values
        result = format_payment_item_values(
            [(u'Item 1', 'USD', decimal.Decimal("12.123")),
             (u'Item 2', 'USD', decimal.Decimal("12.002")),
             ], 'USD')
        self.assertEqual(
            result, [(u'Item 1', 'USD 12.12'),
                     (u'Item 2', 'USD 12.00'),
                     (u'Total', 'USD 24.12')]
            )

    def test_format_payment_item_values_req_single_currency(self):
        # we require one currency for all items, yet.
        self.assertRaises(
            ValueError, format_payment_item_values,
            [(u'Item 1', 'USD', decimal.Decimal("12.12")),
             (u'Item 2', 'EUR', decimal.Decimal("50")),
             ],
            'USD')

    def test_format_amount(self):
        # we can make amounts readable
        D = decimal.Decimal
        self.assertEqual(format_amount(D("0"), 'USD'), u"US$ 0.00")
        self.assertEqual(format_amount(D("0.1"), 'EUR'), u"€ 0.10")
        self.assertEqual(format_amount(D("-1.2"), 'NGN'), u"₦ -1.20")
        self.assertEqual(format_amount(D("1234.5"), 'YEN'), u"YEN 1,234.50")


class FunctionalHelperTests(FunctionalTestCase):

    layer = FunctionalLayer

    def test_services_lister_is_registered(self):
        # a lister of gateway services is registered on startup
        util = queryUtility(IPaymentGatewayServicesLister)
        assert util is not None

    def test_services_are_really_listed(self):
        # we can really get locally registered gateways when calling
        util = queryUtility(IPaymentGatewayServicesLister)
        assert len(util()) > 0

    def test_get_payment(self):
        # we can lookup payments.
        self.getRootFolder()['app'] = Company()
        app = self.getRootFolder()['app']
        setSite(app)
        p1 = Payment(FakePayer(), FakePayable())
        app['payments']['1'] = p1
        p_id = p1.payment_id
        result = get_payment(p_id)
        self.assertTrue(result is p1)
        self.assertTrue(get_payment('not-valid') is None)

    def test_get_payments_from_payer_id(self):
        # we can lookup payments from payer ids.
        self.getRootFolder()['app'] = Company()
        app = self.getRootFolder()['app']
        setSite(app)
        p1 = Payment(FakePayer(), FakePayable())
        app['payments']['1'] = p1
        result = get_payments_from_payer_id('PAYER_01')
        self.assertTrue(result[0] is p1)
        self.assertTrue(get_payments_from_payer_id('not-valid') is None)

    def test_get_payments_from_payable_id(self):
        # we can lookup payments from payable ids.
        self.getRootFolder()['app'] = Company()
        app = self.getRootFolder()['app']
        setSite(app)
        p1 = Payment(FakePayer(), FakePayable())
        app['payments']['1'] = p1
        result = get_payments_from_payable_id('PAYABLE_01')
        self.assertTrue(result[0] is p1)
        self.assertTrue(get_payments_from_payable_id('not-valid') is None)

    def test_find_payable_from_payable_id(self):
        # we can find payables.
        obj1 = object()
        obj2 = object()

        class FakeFinder(object):
            valid = {'id1': obj1, 'id3': obj2}

            def get_payable_by_id(self, the_id):
                return self.valid.get(the_id)

        finder1 = FakeFinder()
        finder1.valid = {'id1': obj1}
        finder2 = FakeFinder()
        finder2.valid = {'id2': obj2}
        gsm = getGlobalSiteManager()
        try:
            gsm.registerUtility(finder1, provided=IPayableFinder, name='f1')
            gsm.registerUtility(finder2, provided=IPayableFinder, name='f2')
            result1 = find_payable_from_payable_id('id1')
            result2 = find_payable_from_payable_id('id2')
            result3 = find_payable_from_payable_id('id3')
        finally:
            gsm.unregisterUtility(finder1, IPayableFinder)
            gsm.unregisterUtility(finder2, IPayableFinder)
        self.assertTrue(result1 is obj1)
        self.assertTrue(result2 is obj2)
        self.assertTrue(result3 is None)

    def test_find_payer_from_payer_id(self):
        # we can find payables.
        obj1 = object()
        obj2 = object()

        class FakeFinder(object):
            valid = {'id1': obj1, 'id3': obj2}

            def get_payer_by_id(self, the_id):
                return self.valid.get(the_id)

        finder1 = FakeFinder()
        finder1.valid = {'id1': obj1}
        finder2 = FakeFinder()
        finder2.valid = {'id2': obj2}
        gsm = getGlobalSiteManager()
        try:
            gsm.registerUtility(finder1, provided=IPayerFinder, name='f1')
            gsm.registerUtility(finder2, provided=IPayerFinder, name='f2')
            result1 = find_payer_from_payer_id('id1')
            result2 = find_payer_from_payer_id('id2')
            result3 = find_payer_from_payer_id('id3')
        finally:
            gsm.unregisterUtility(finder1, IPayerFinder)
            gsm.unregisterUtility(finder2, IPayerFinder)
        self.assertTrue(result1 is obj1)
        self.assertTrue(result2 is obj2)
        self.assertTrue(result3 is None)


class PaymentTests(FunctionalTestCase):

    layer = FunctionalLayer

    def setUp(self):
        super(PaymentTests, self).setUp()
        self.payer = FakePayer()
        self.payable = FakePayable()

    def test_iface(self):
        # Payments fullfill any interface contracts
        obj = Payment(self.payer, self.payable)
        verifyClass(IPayment, Payment)
        verifyObject(IPayment, obj)

    def test_initial_values(self):
        # important attributes are set initially
        payer = self.payer
        payer.payer_id = u'PAYER_ID'
        payable = self.payable
        payable.payable_id = u'PAYABLE_ID'
        payable.title = u'PAYABLE-TITLE'
        payable.currency = 'NGN'
        payment = Payment(payer, payable)
        assert payment.payer_id == u'PAYER_ID'
        assert payment.payable_id == u'PAYABLE_ID'
        assert payment.title == u'PAYABLE-TITLE'
        assert payment.currency == 'NGN'
        assert isinstance(payment.creation_date, datetime.datetime)
        assert payment.payment_date is None

    def test_payment_id_unique(self):
        # we get unique payment ids
        p1 = Payment(self.payer, self.payable)
        p2 = Payment(self.payer, self.payable)
        id1, id2 = p1.payment_id, p2.payment_id
        assert id1 != id2

    def test_payment_id_format(self):
        # payment ids have a special format: "PAY_<32 hex digits>"
        id1 = Payment(self.payer, self.payable).payment_id
        assert isinstance(id1, basestring)
        assert re.match('PAY_[0-9a-f]{32}', id1)

    def test_initial_state_is_unpaid(self):
        # the initial state of payments is <unpaid>
        p1 = Payment(self.payer, self.payable)
        assert p1.state == STATE_UNPAID

    def test_approve(self):
        # we can approve payments
        p1 = Payment(self.payer, self.payable)
        p1.approve()
        assert p1.state == STATE_PAID
        assert p1.payment_date is not None
        assert isinstance(p1.payment_date, datetime.datetime)

    def test_approve_datetime_given(self):
        # we can give a datetime
        p1 = Payment(self.payer, self.payable)
        some_datetime = datetime.datetime(2014, 1, 1, 0, 0, 0)
        p1.approve(payment_date=some_datetime)
        assert p1.payment_date == some_datetime

    def test_approve_datetime_automatic(self):
        # if we do not give a datetime, current one will be used
        current = datetime.datetime.utcnow()
        p1 = Payment(self.payer, self.payable)
        p1.approve()
        assert p1.payment_date >= current

    def test_mark_failed(self):
        # we can mark payments as failed
        p1 = Payment(self.payer, self.payable)
        p1.mark_failed()
        assert p1.state == STATE_FAILED

    def test_amount(self):
        # the amount of a payment is the sum of amounts of its items
        payable = self.payable
        payable.payment_items[0].amount = decimal.Decimal("12.25")
        payable.payment_items[1].amount = decimal.Decimal("0.5")
        p1 = Payment(self.payer, self.payable)
        assert p1.amount == decimal.Decimal("12.75")

    def test_amount_negative(self):
        # we can sum up negative numbers
        payable = self.payable
        payable.payment_items[0].amount = decimal.Decimal("2.21")
        payable.payment_items[1].amount = decimal.Decimal("-3.23")
        p1 = Payment(self.payer, payable)
        assert p1.amount == decimal.Decimal("-1.02")

    def test_amount_empty(self):
        # the amount of zero items is None.
        payable = FakePayable(payment_items=())
        p1 = Payment(self.payer, payable)
        self.assertEqual(p1.amount, decimal.Decimal("0.00"))

    def test_amount_changed_after_init(self):
        # amount is dynamic: changes to payment items are reflected
        payable = FakePayable(payment_items=())
        p1 = Payment(self.payer, payable)
        assert p1.amount == decimal.Decimal("0.00")
        p1.payment_items = (PaymentItem(amount=decimal.Decimal("5.55")), )
        assert p1.amount == decimal.Decimal("5.55")
        p1.payment_items = (
            PaymentItem(amount=decimal.Decimal("5.55")),
            PaymentItem(amount=decimal.Decimal("1.11")),
            )
        assert p1.amount == decimal.Decimal("6.66")

    def test_payment_items(self):
        # we can get payment items from a payment
        payable = FakePayable(payment_items=FAKE_PAYMENT_ITEMS)
        p1 = Payment(self.payer, payable)
        assert isinstance(p1.payment_items, tuple)

    def test_payment_items_number(self):
        # payment item values are respected
        payable = FakePayable()
        item = PaymentItem(amount=decimal.Decimal("9.99"))
        payable.payment_items = [item, ]
        payment = Payment(self.payer, payable)
        assert len(payment.payment_items) == 1
        assert payment.payment_items[0] is item


class PaymentItemTests(unittest.TestCase):

    def test_iface(self):
        # PaymentItems fullfill any interface contracts
        obj = PaymentItem()
        verifyClass(IPaymentItem, PaymentItem)
        verifyObject(IPaymentItem, obj)
