# -*- coding: utf-8 -*-
from decimal import Decimal as D
from unittest import mock

import responses
from django.db import IntegrityError
from django.db import transaction
from django.test import TestCase
from django.test.utils import override_settings
from django.urls import reverse
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_encode

from oscar.apps.offer.applicator import Applicator
from oscar.apps.payment.models import Source, SourceType
from oscar.core.loading import get_class, get_model
from oscar.test import factories

import ocyan.core.fender
from ocyan.plugin.oscar.pipeline import (
    PIPELINE_STATUS_CANCELLED,
    PIPELINE_STATUS_NEW,
    PIPELINE_STATUS_PAID,
    PIPELINE_STATUS_PENDING_PAYMENT,
)
from ocyan.plugin.oscar.utils import create_or_record_payment_events
from ocyan.plugin.payment_mollie.mollie_payment import MolliePayment
from ocyan.plugin.payment_mollie.models import MollieWebhookEvent

OCYAN_MOCK_CONFIG = {"oscar": {}, "payment_mollie": {"api_key": "test_key"}}
Selector = get_class("partner.strategy", "Selector")
EventHandler = get_class("order.processing", "EventHandler")
Basket = get_model("basket", "Basket")
Order = get_model("order", "Order")
ShippingAddress = get_model("order", "ShippingAddress")
Surcharge = get_model("order", "Surcharge")
ShippingEventType = get_model("order", "ShippingEventType")


def create_callback_url(order):
    oidb64 = urlsafe_base64_encode(force_bytes(order.pk))
    token = order.verification_hash()

    return reverse(
        "payment_mollie:mollie_callback", kwargs={"oidb64": oidb64, "token": token}
    )


class MollieWebhookEventTests(TestCase):
    def test_classic_webhook_rejects_malformed_references_without_provider_call(self):
        response = self.client.post(reverse("payment_mollie:webhook"), {"id": "bad"})
        self.assertEqual(response.status_code, 200)

    def test_webhook_event_fingerprint_is_unique_per_provider_reference(self):
        MollieWebhookEvent.objects.create(
            provider_reference="tr_test", fingerprint="a" * 64, status="paid"
        )
        with self.assertRaises(IntegrityError):
            with transaction.atomic():
                MollieWebhookEvent.objects.create(
                    provider_reference="tr_test", fingerprint="a" * 64, status="paid"
                )

    @mock.patch("ocyan.plugin.payment_mollie.views.MolliePayment")
    def test_duplicate_webhook_delivery_is_processed_once(self, mollie_payment_class):
        mollie_payment = mollie_payment_class.return_value
        mollie_payment.mollie.payments.get.return_value = {
            "id": "tr_test",
            "status": "paid",
            "amount": {"value": "10.00"},
        }

        first = self.client.post(reverse("payment_mollie:webhook"), {"id": "tr_test"})
        second = self.client.post(reverse("payment_mollie:webhook"), {"id": "tr_test"})

        self.assertEqual(first.status_code, 200)
        self.assertEqual(second.status_code, 200)
        self.assertEqual(mollie_payment.update_payment_status.call_count, 1)
        self.assertEqual(MollieWebhookEvent.objects.count(), 1)


@override_settings(ALLOWED_HOSTS=["*"])
@mock.patch.object(ocyan.core.fender.config, "settings", OCYAN_MOCK_CONFIG)
class MolliePaymentTest(TestCase):
    fixtures = [
        "payment_mollie/offer",
        "payment_mollie/order",
        "payment_mollie/address",
        "payment_mollie/partner",
        "payment_mollie/catalogue",
    ]
    webhook = reverse("payment_mollie:webhook")
    ideal_url = reverse("payment_mollie:ideal")

    def create_basket(self, empty=False):
        basket = Basket.objects.create()
        basket.strategy = Selector().strategy()
        if not empty:
            product = factories.create_product()
            factories.create_stockrecord(product, num_in_stock=2)
            basket.add_product(product)
        return basket

    @responses.activate
    def test_create_payment(self, order=None):
        responses.add(
            responses.POST,
            "https://api.mollie.com/v2/payments",
            json={"id": "tr_inggg", "status": "cancelled"},
            status=200,
            content_type="application/json",
        )

        responses.add(
            responses.GET,
            "https://api.mollie.com/v2/payments/tr_inggg",
            json={
                "id": "tr_inggg",
                "status": "cancelled",
                "createdDatetime": "2018-10-23T07:30:46.0Z",
                "paidDatetime": "2018-10-23T07:30:51.0Z",
                "amount": {"value": "10.00", "currency": "EUR"},
                "method": "ideal",
                "_links": {
                    "checkout": {
                        "href": "https://www.mollie.com/payscreen/select-method/WDqYK6vllg",
                        "type": "text/html",
                    }
                },
            },
            status=200,
            content_type="application/json",
        )
        if order is None:
            order = factories.create_order(
                guest_email="admin@admin.nl",
                status=PIPELINE_STATUS_NEW,
                basket=self.create_basket(),
            )

        mollie_payment = MolliePayment()
        url = mollie_payment.create_payment(
            order=order,
            method="ideal",
            webhook=self.webhook,
            redirect=create_callback_url(order),
            description=None,
        )

        self.assertEqual(
            url, "https://www.mollie.com/payscreen/select-method/WDqYK6vllg"
        )
        order.refresh_from_db()
        self.assertEqual(order.status, PIPELINE_STATUS_PENDING_PAYMENT)
        self.assertGreaterEqual(
            order.payment_events.filter(
                event_type__name=PIPELINE_STATUS_PENDING_PAYMENT
            ).count(),
            1,
        )

    def test_no_get(self):
        response = self.client.get(self.webhook)
        self.assertEqual(response.status_code, 405)

    @responses.activate
    def test_webhook(self):
        self.test_create_payment()
        responses.start()
        responses.add(
            responses.GET,
            "https://api.mollie.com/v2/payments/tr_inggg",
            json={
                "id": "tr_inggg",
                "status": "cancelled",
                "createdDatetime": "2018-10-23T07:30:46.0Z",
                "paidDatetime": "2018-10-23T07:30:51.0Z",
                "amount": {"value": "10.00", "currency": "EUR"},
                "method": "ideal",
                "links": {
                    "checkout": {
                        "href": "https://www.mollie.com/payscreen/select-method/WDqYK6vllg",
                        "type": "text/html",
                    }
                },
            },
            status=200,
            content_type="application/json",
        )

    @responses.activate
    def test_webhook_different_method(self):
        responses.add(
            responses.POST,
            "https://api.mollie.com/v2/payments",
            json={"id": "tr_inggg", "status": "pending"},
            status=200,
            content_type="application/json",
        )
        responses.add(
            responses.GET,
            "https://api.mollie.com/v2/payments/tr_inggg",
            json={
                "id": "tr_inggg",
                "status": "pending",
                "createdDatetime": "2018-10-23T07:30:46.0Z",
                "paidDatetime": "2018-10-23T07:30:51.0Z",
                "amount": {"value": "9.99", "currency": "EUR"},
                "method": "geit",
                "_links": {
                    "checkout": {
                        "href": "https://www.mollie.com/payscreen/select-method/WDqYK6vllg",
                        "type": "text/html",
                    }
                },
            },
            status=200,
            content_type="application/json",
        )

        order = factories.create_order(
            guest_email="admin@admin.nl",
            status=PIPELINE_STATUS_NEW,
            basket=self.create_basket(),
        )

        mollie_payment = MolliePayment()
        mollie_payment.create_payment(
            order=order,
            method="ideal",
            webhook=self.webhook,
            redirect=create_callback_url(order),
            description=None,
        )

        source = order.sources.get()

        self.assertIn("ideal", source.source_type.code)

        self.client.post(self.webhook, {"id": "tr_inggg"})
        source.refresh_from_db()

        self.assertEqual(order.sources.count(), 1)
        self.assertIn("geit", source.source_type.code)

    @responses.activate
    def test_mollie_payment_view(self):
        responses.add(
            responses.POST,
            "https://api.mollie.com/v2/payments",
            json={"id": "tr_inggg", "status": "cancelled"},
            status=200,
            content_type="application/json",
        )

        # Mock create customer
        responses.add(
            responses.POST,
            "https://api.mollie.com/v2/customers",
            json={"id": "tr_inggg"},
            status=200,
            content_type="application/json",
        )

        # Mock get payment_url
        responses.add(
            responses.GET,
            "https://api.mollie.com/v2/payments/tr_inggg",
            json={
                "_links": {
                    "checkout": {
                        "href": "http://shop.welkombij.highbiza.nl/payeer/inggg"
                    }
                }
            },
            status=200,
            content_type="application/json",
        )

        order = factories.create_order(
            status=PIPELINE_STATUS_PENDING_PAYMENT,
            basket=self.create_basket(),
            guest_email="barry@batsbak.nl",
        )
        session = self.client.session
        session["checkout_order_id"] = order.pk
        session.save()
        response = self.client.get(self.ideal_url)
        self.assertEqual(response.status_code, 302)

    @responses.activate
    def test_callback_view(self):
        responses.add(
            responses.POST,
            "https://api.mollie.com/v2/payments",
            json={"id": "tr_inggg"},
            status=200,
            content_type="application/json",
        )

        responses.add(
            responses.GET,
            "https://api.mollie.com/v2/payments/tr_inggg",
            json={
                "id": "tr_inggg",
                "status": "paid",
                "createdDatetime": "2018-10-23T07:30:46.0Z",
                "paidDatetime": "2018-10-23T07:30:51.0Z",
                "amount": {"value": "29.50"},
                "method": "ideal",
                "links": {
                    "redirectUrl": "http://test.local.nl:8000/shop/payment/mollie-recurrent/callback/"
                },
            },
            status=200,
            content_type="application/json",
        )

        responses.add(
            responses.POST,
            "https://api.mollie.com/v2/customers",
            json={"id": "tr_inggg"},
            status=200,
            content_type="application/json",
        )

        responses.add(
            responses.GET,
            "https://api.mollie.com/v1/customers/tr_inggg/mandates",
            json={
                "data": [
                    {
                        "resource": "mandate",
                        "id": "mdt_B2FwRpwMUw",
                        "mode": "test",
                        "status": "valid",
                        "customerId": "tr_inggg",
                    }
                ]
            },
            status=200,
            content_type="application/json",
        )

        source_type = SourceType.objects.create(code="mollie:ideal", name="Mollie")
        order = factories.create_order(
            status=PIPELINE_STATUS_PENDING_PAYMENT,
            basket=self.create_basket(),
            guest_email="barry@batsbak.nl",
        )
        source = Source(
            order=order,
            source_type=source_type,
            amount_allocated=D("10.00"),
            currency="EUR",
            reference="tr_inggg",
        )
        source.allocate(
            amount=D("0.00"),
            reference="tr_inggg",
            status=PIPELINE_STATUS_PENDING_PAYMENT,
        )
        session = self.client.session
        session["checkout_order_id"] = order.pk
        session.save()
        response = self.client.get(create_callback_url(order))
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.url, order.get_thank_you_url())

    @responses.activate
    @mock.patch("ocyan.mollie.mixins.send_order_confirmation_message")
    def test_callback_view_banktransfer(self, send_order_confirmation_message_mock):
        responses.add(
            responses.POST,
            "https://api.mollie.com/v2/payments",
            json={"id": "tr_inggg"},
            status=200,
            content_type="application/json",
        )

        responses.add(
            responses.GET,
            "https://api.mollie.com/v2/payments/tr_inggg",
            json={
                "id": "tr_inggg",
                "status": "open",
                "createdDatetime": "2018-10-23T07:30:46.0Z",
                "paidDatetime": "2018-10-23T07:30:51.0Z",
                "amount": {"value": "29.50"},
                "method": "banktransfer",
                "links": {
                    "redirectUrl": "http://test.local.nl:8000/shop/payment/mollie-recurrent/callback/"
                },
            },
            status=200,
            content_type="application/json",
        )

        responses.add(
            responses.POST,
            "https://api.mollie.com/v2/customers",
            json={"id": "tr_inggg"},
            status=200,
            content_type="application/json",
        )

        responses.add(
            responses.GET,
            "https://api.mollie.com/v1/customers/tr_inggg/mandates",
            json={
                "data": [
                    {
                        "resource": "mandate",
                        "id": "mdt_B2FwRpwMUw",
                        "mode": "test",
                        "status": "valid",
                        "customerId": "tr_inggg",
                    }
                ]
            },
            status=200,
            content_type="application/json",
        )

        source_type = SourceType.objects.create(
            code="mollie:banktransfer", name="Mollie"
        )
        order = factories.create_order(
            status=PIPELINE_STATUS_PENDING_PAYMENT,
            basket=self.create_basket(),
            guest_email="barry@batsbak.nl",
        )
        source = Source(
            order=order,
            source_type=source_type,
            amount_allocated=D("10.00"),
            currency="EUR",
            reference="tr_inggg",
        )
        source.allocate(
            amount=D("0.00"),
            reference="tr_inggg",
            status=PIPELINE_STATUS_PENDING_PAYMENT,
        )
        session = self.client.session
        session["checkout_order_id"] = order.pk
        session.save()
        response = self.client.get(create_callback_url(order))
        self.assertEqual(response.status_code, 302)

        send_order_confirmation_message_mock.assert_called_once_with(
            "ORDER_PLACED", order
        )

        self.assertEqual(response.url, order.get_thank_you_url())

    @responses.activate
    @mock.patch.object(ocyan.core.fender.config, "settings", OCYAN_MOCK_CONFIG)
    def test_payment_with_order_api_as_guest(self):
        shipping_addr = ShippingAddress.objects.get(pk=1)
        order = factories.create_order(
            guest_email="admin@admin.nl",
            shipping_address=shipping_addr,
            status=PIPELINE_STATUS_NEW,
            basket=self.create_basket(),
        )

        order_return_value = {
            "resource": "order",
            "id": "ord_wpk8g5",
            "profileId": "pfl_SvRxe7tKnC",
            "method": "klarnapaylater",
            "status": "created",
        }
        responses.add(
            responses.GET, "https://api.mollie.com/v2/orders/ord_wpk8g5", json={}
        )

        mol = MolliePayment()
        with mock.patch(
            "mollie.api.resources.orders.Orders.create", return_value=order_return_value
        ) as mollie_order_create:
            mol.create_mollie_order(
                order=order,
                method="klarnapaylater",
                webhook=self.webhook,
                redirect=create_callback_url(order),
            )

            self.assertTrue(mollie_order_create.called)

        # Existing ``ord_`` records remain a reconciliation-only contract.
        # New checkout creation is covered by the Payments API tests below.

    @mock.patch.object(ocyan.core.fender.config, "settings", OCYAN_MOCK_CONFIG)
    def test_get_billing_address(self):
        shipping_addr = ShippingAddress.objects.get(pk=1)
        billing_addr = factories.order.BillingAddressFactory()
        billing_addr.first_name = "kapitein"
        billing_addr.last_name = "haak"
        billing_addr.email = "haak@highbiza.nl"
        order = factories.create_order(
            guest_email="admin@admin.nl",
            shipping_address=shipping_addr,
            billing_address=billing_addr,
            status=PIPELINE_STATUS_NEW,
            basket=self.create_basket(),
        )
        mol = MolliePayment()
        bil_addr = mol.get_billing_address(order)
        self.assertEqual(bil_addr["givenName"], billing_addr.first_name)
        self.assertEqual(bil_addr["familyName"], billing_addr.last_name)
        self.assertEqual(bil_addr["email"], billing_addr.email)

    @responses.activate
    @mock.patch.object(ocyan.core.fender.config, "settings", OCYAN_MOCK_CONFIG)
    def test_order_api_with_surcharges_and_shipping(self):
        shipping_addr = ShippingAddress.objects.get(pk=1)
        order = factories.create_order(
            guest_email="admin@admin.nl",
            shipping_address=shipping_addr,
            status=PIPELINE_STATUS_NEW,
            basket=self.create_basket(),
        )

        Surcharge.objects.create(
            order=order, name="toeslag", code="toeslag", incl_tax=5, excl_tax=4
        )
        Surcharge.objects.create(
            order=order, name="toeslag_2", code="toeslag_2", incl_tax=12.10, excl_tax=10
        )
        order.shipping_incl_tax = 15
        order.shipping_excl_tax = 10
        order_return_value = {
            "resource": "order",
            "id": "ord_wpk8g5",
            "profileId": "pfl_SvRxe7tKnC",
            "method": "klarnapaylater",
            "status": "created",
        }
        responses.add(
            responses.GET, "https://api.mollie.com/v2/orders/ord_wpk8g5", json={}
        )

        mol = MolliePayment()
        with mock.patch(
            "mollie.api.resources.orders.Orders.create", return_value=order_return_value
        ) as mollie_order_create:
            mol.create_mollie_order(
                order=order,
                method="klarnapaylater",
                webhook=self.webhook,
                redirect=create_callback_url(order),
            )

            payload = mollie_order_create.call_args.args[0]
            self.assertEqual(payload["amount"], {"currency": "EUR", "value": "9.99"})
            self.assertEqual(payload["method"], "klarnapaylater")
            self.assertEqual(payload["lines"][1]["unitPrice"]["value"], "15.00")
            self.assertEqual(payload["lines"][1]["vatAmount"]["value"], "5.00")
            self.assertEqual(payload["billingAddress"]["country"], "NL")

    @responses.activate
    @mock.patch.object(ocyan.core.fender.config, "settings", OCYAN_MOCK_CONFIG)
    def test_order_api_with_discount(self):
        basket = self.create_basket()
        line = basket.lines.all().first()
        line.quantity = 3
        line.save()

        applicator = Applicator()
        applicator.apply(basket)

        shipping_addr = ShippingAddress.objects.get(pk=1)
        order = factories.create_order(
            guest_email="admin@admin.nl",
            shipping_address=shipping_addr,
            status=PIPELINE_STATUS_NEW,
            basket=basket,
        )

        order_return_value = {
            "resource": "order",
            "id": "ord_wpk8g5",
            "profileId": "pfl_SvRxe7tKnC",
            "method": "klarnapaylater",
            "status": "authorized",
        }
        responses.add(
            responses.GET, "https://api.mollie.com/v2/orders/ord_wpk8g5", json={}
        )

        mol = MolliePayment()
        with mock.patch(
            "mollie.api.resources.orders.Orders.create", return_value=order_return_value
        ) as mollie_order_create:
            mol.create_mollie_order(
                order=order,
                method="klarnapaylater",
                webhook=self.webhook,
                redirect=create_callback_url(order),
            )

            called_args = mollie_order_create.call_args[0][0]
            self.assertEqual(called_args["lines"][0]["quantity"], 1)
            self.assertLess(D(called_args["lines"][0]["unitPrice"]["value"]), D("9.99"))
            self.assertRegex(
                called_args["lines"][0]["vatAmount"]["value"], r"^\d+\.\d{2}$"
            )
            self.assertEqual(len(called_args["lines"]), 2)
            self.assertRegex(called_args["amount"]["value"], r"^\d+\.\d{2}$")

    @responses.activate
    @mock.patch.object(ocyan.core.fender.config, "settings", OCYAN_MOCK_CONFIG)
    def test_create_and_cancel_payment(self, payment_id="tr_wpk8g5", order=None):
        if order is None:
            order = factories.create_order(
                guest_email="admin@admin.nl",
                status=PIPELINE_STATUS_NEW,
                basket=self.create_basket(),
            )
        payment_return_value = {
            "resource": "payment",
            "id": payment_id,
            "profileId": "pfl_SvRxe7tKnC",
            "method": "ideal",
            "status": "henk",
            "_links": {"checkout": {"href": "https://checkout.example/pay"}},
        }

        mol = MolliePayment()
        responses.add(
            responses.GET, f"https://api.mollie.com/v2/payments/{payment_id}", json={}
        )

        with mock.patch(
            "mollie.api.resources.payments.Payments.create",
            return_value=payment_return_value,
        ) as _:
            mol.create_payment(
                order=order,
                method="ideal",
                webhook=self.webhook,
                redirect=create_callback_url(order),
                description=None,
            )
        order.refresh_from_db()
        self.assertEqual(order.status, PIPELINE_STATUS_PENDING_PAYMENT)
        self.assertGreaterEqual(
            order.payment_events.filter(
                event_type__name=PIPELINE_STATUS_PENDING_PAYMENT
            ).count(),
            1,
        )

        mol.cancel_payment_event(order, payment_id)
        self.assertEqual(order.status, PIPELINE_STATUS_PENDING_PAYMENT)
        self.assertGreaterEqual(
            order.payment_events.filter(
                event_type__name=PIPELINE_STATUS_CANCELLED
            ).count(),
            1,
        )

    @mock.patch.object(ocyan.core.fender.config, "settings", OCYAN_MOCK_CONFIG)
    def test_paid_after_trying_three_times(self):
        order = factories.create_order(
            guest_email="admin@admin.nl",
            status=PIPELINE_STATUS_NEW,
            basket=self.create_basket(),
        )
        self.test_create_and_cancel_payment(order=order)
        self.test_create_and_cancel_payment(payment_id="tr_wpk8g6", order=order)
        self.test_create_payment(order=order)
        create_or_record_payment_events(order, PIPELINE_STATUS_PAID, "paid", "tr_inggg")

        order.refresh_from_db()
        self.assertEqual(
            order.payment_events.filter(
                event_type__name=PIPELINE_STATUS_PENDING_PAYMENT
            ).count(),
            3,
        )
        self.assertEqual(
            order.payment_events.filter(
                event_type__name=PIPELINE_STATUS_CANCELLED
            ).count(),
            2,
        )
        self.assertEqual(
            order.payment_events.filter(event_type__name=PIPELINE_STATUS_PAID).count(),
            1,
        )
        self.assertEqual(order.status, PIPELINE_STATUS_PAID)

    @mock.patch.object(ocyan.core.fender.config, "settings", OCYAN_MOCK_CONFIG)
    def test_order_api_with_local_vat_rate(self):
        """the product used for this test has tax rate low"""
        mol = MolliePayment()
        order = Order.objects.get(pk=1)
        with mock.patch.object(mol.mollie.orders, "create") as mock_method:
            with mock.patch(
                "ocyan.plugin.oscar_partner.partner.utils.is_local_vat_applicable",
                return_value=True,
            ):
                mol.create_mollie_order(
                    order,
                    "klarnapaylater",
                    "example.com/webhook",
                    "example.com/redirect",
                )
                vat_rate = mock_method.call_args.args[0]["lines"][0]["vatRate"]
                country = mock_method.call_args.args[0]["billingAddress"]["country"]
        self.assertEqual(country, "BE")
        self.assertEqual(vat_rate, "6.00")

    @mock.patch.object(ocyan.core.fender.config, "settings", OCYAN_MOCK_CONFIG)
    def test_order_api_vat_rate(self):
        """
        taxbase.is_local_vat_applicable should return false in this test
        the product used for this test has tax rate low
        """
        mol = MolliePayment()
        order = Order.objects.get(pk=1)
        with mock.patch.object(mol.mollie.orders, "create") as mock_method:
            mol.create_mollie_order(
                order, "klarnapaylater", "example.com/webhook", "example.com/redirect"
            )
            vat_rate = mock_method.call_args.args[0]["lines"][0]["vatRate"]
            country = mock_method.call_args.args[0]["billingAddress"]["country"]
        self.assertEqual(country, "BE")
        self.assertEqual(vat_rate, "9.00")

    @responses.activate
    @mock.patch.object(ocyan.core.fender.config, "settings", OCYAN_MOCK_CONFIG)
    def test_handle_shipping_event_with_mollie(self):
        order = factories.create_order(
            guest_email="admin@admin.nl",
            status=PIPELINE_STATUS_NEW,
            basket=self.create_basket(),
        )
        source_type = SourceType.objects.create(code="mollie:ideal", name="Mollie")
        Source.objects.create(
            order=order,
            source_type=source_type,
            reference="ord_wpk8g5",
            amount_allocated=D("10.00"),
            currency="EUR",
        )

        # Mock Mollie API responses
        responses.add(
            responses.GET,
            "https://api.mollie.com/v2/orders/ord_wpk8g5",
            json={"id": "ord_wpk8g5", "status": "authorized"},
        )
        responses.add(
            responses.POST,
            "https://api.mollie.com/v2/orders/ord_wpk8g5/shipments",
            json={"resource": "shipment", "id": "shp_3wmsgCJN4U"},
        )

        # Create shipping event
        lines = order.lines.all()
        line_quantities = [line.quantity for line in lines]

        event = EventHandler().handle_shipping_event(
            order=order,
            event_type=ShippingEventType.objects.first(),
            lines=lines,
            line_quantities=line_quantities,
        )

        self.assertIsNotNone(event)
