from decimal import Decimal as D
from unittest import mock

from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser
from django.test import TestCase
from django.urls import reverse
from phonenumber_field.phonenumber import PhoneNumber
from rest_framework.reverse import reverse as api_reverse
from rest_framework.settings import api_settings
from rest_framework.test import APIRequestFactory

from oscar.apps.checkout.applicator import SurchargeApplicator
from oscar.core.loading import get_model

import ocyan.core.fender
from ocyan.plugin.wagtail_utils.utils import get_wagtailsite

from ocyan.plugin.oscar_checkout.checkoutapi import serializers

Basket = get_model("basket", "Basket")
Country = get_model("address", "Country")
PaymentMethod = get_model("checkout", "PaymentMethod")
ShippingMethod = get_model("shipping", "OrderAndItemCharges")
FixedSurcharge = get_model("checkout", "FixedSurcharge")
PercentageSurcharge = get_model("checkout", "PercentageSurcharge")
User = get_user_model()
factory = APIRequestFactory()


CHECKOUTAPI_OCYAN_OPTIONS = {"shipping": {"enable_charged_shipping": True}}


class TestSurchargeApplicator(SurchargeApplicator):
    def get_surcharges(self, basket, **kwargs):
        return (
            FixedSurcharge(price=D(20), vat_rate="high"),
            PercentageSurcharge(percentage=10, include_shipping_charges=True),
        )


class CheckoutApiTest(TestCase):
    fixtures = [
        "checkout/auth",
        "checkout/catalogue",
        "checkout/shipping",
        "checkout/paymentmethod",
    ]

    def fill_basket(self):
        henk = User.objects.get(username="henk")
        self.client.force_login(henk)
        self.assertEqual(Basket.objects.count(), 0, "The basket should be empty")

        basket_add_url = reverse("basket:add", args=(1,))
        body = {"quantity": 3}

        response = self.client.post(basket_add_url, body)
        self.assertEqual(
            response.status_code, 302, "Adding an item to a basket should redirect."
        )

    def test_shipping_methods(self):
        body = {
            "country": "http://testserver/checkout/api/countries/NL/",
            "first_name": "dsfsdf",
            "house_number": "",
            "last_name": "sdfsdfsd",
            "line1": "Doctor Coppesstraat 29",
            "line2": "",
            "line3": "",
            "line4": "sdfsd",
            "notes": "",
            "phone_number": "",
            "postcode": "7523EJ",
            "state": "",
            "title": "",
        }
        url = api_reverse("checkoutapi:api-basket-shipping-methods", format="html")
        response = self.client.post(
            url, body, content_type="application/json; charset=UTF-8"
        )
        self.assertEqual(response.status_code, 200)

    def test_total_call(self):
        self.fill_basket()

        body = {
            "shipping_address": {
                "country": "http://testserver/checkout/api/countries/NL/"
            },
            "shipping_method_code": "fedex",
        }

        url = reverse("checkoutapi:api-basket-total")
        response = self.client.post(
            url, body, content_type="application/json; charset=UTF-8"
        )
        self.assertEqual(response.status_code, 200)


class CheckoutSerializerTest(CheckoutApiTest):
    def get_request(self, user):
        request = factory.post("/checkout/")
        request.user = user
        if api_settings.DEFAULT_VERSIONING_CLASS is not None:
            versioning_class = api_settings.DEFAULT_VERSIONING_CLASS
            request.version = "checkoutapi"
            request.versioning_scheme = versioning_class()
        request.site = get_wagtailsite("example.com")
        return request

    def test_fill_basket(self):
        self.fill_basket()

    def test_shipping(self):
        self.assertEqual(ShippingMethod.objects.count(), 1)

    def get_data(self):
        return {
            "basket": "http://testserver/checkout/api/baskets/1/",
            "shipping_address": {
                "title": "Mr",
                "first_name": "Henk",
                "last_name": "Bertenbrau",
                "line1": "Klooiensloot",
                "house_number": "23",
                "line2": "Achter het hek",
                "line3": "2de gebbouw",
                "line4": "Arnhem",
                "state": "Gelderland",
                "postcode": "6811AL",
                "country": "http://testserver/checkout/api/countries/NL/",
                "phone_number": "0612345678",
                "notes": "",
            },
            "shipping_method_code": "fedex",
            "shipping_charge": {
                "currency": "EUR",
                "incl_tax": "5.95",
                "excl_tax": "4.9174",
                "tax": "1.0326",
            },
            "guest_email": "henk@lol.com",
        }

    def get_bare_data(self):
        return {
            "basket": "http://testserver/checkout/api/baskets/1/",
            "shipping_address": {
                "country": "http://testserver/checkout/api/countries/NL/"
            },
            "shipping_method_code": "fedex",
        }

    @mock.patch.object(ocyan.core.fender.config, "settings", CHECKOUTAPI_OCYAN_OPTIONS)
    def test_no_surcharges_anonymous(self):
        self.test_fill_basket()

        with self.settings(OSCAR_ALLOW_ANON_CHECKOUT=True):
            ser = serializers.CheckoutSerializer(
                data=self.get_data(),
                context={"request": self.get_request(AnonymousUser())},
            )
            self.assertTrue(ser.is_valid())
            order = ser.save()
            self.assertEqual(order.surcharges.count(), 0)
            self.assertEqual(order.total_incl_tax, D("305.95"))

    @mock.patch(
        "ocyan.plugin.oscar_checkout.checkoutapi.serializers.SurchargeApplicator",
        TestSurchargeApplicator,
    )
    @mock.patch.object(ocyan.core.fender.config, "settings", CHECKOUTAPI_OCYAN_OPTIONS)
    def test_surcharges_anonymous(self):
        self.test_fill_basket()

        with self.settings(OSCAR_ALLOW_ANON_CHECKOUT=True):
            ser = serializers.CheckoutSerializer(
                data=self.get_data(),
                context={"request": self.get_request(AnonymousUser())},
            )
            self.assertTrue(ser.is_valid())
            validated_data = ser.validated_data
            order = ser.create(validated_data)
            self.assertEqual(order.surcharges.count(), 2)
            self.assertEqual(order.total_incl_tax, D("356.55"))

    @mock.patch.object(ocyan.core.fender.config, "settings", CHECKOUTAPI_OCYAN_OPTIONS)
    def test_international_nl_phone_number(self):
        self.test_fill_basket()

        with self.settings(OSCAR_ALLOW_ANON_CHECKOUT=True):
            data = self.get_data()
            data["shipping_address"]["phone_number"] = "+31612345678"
            ser = serializers.CheckoutSerializer(
                data=data, context={"request": self.get_request(AnonymousUser())}
            )
            self.assertTrue(ser.is_valid())

    @mock.patch.object(ocyan.core.fender.config, "settings", CHECKOUTAPI_OCYAN_OPTIONS)
    def test_localized_nl_phone_number(self):
        self.test_fill_basket()

        with self.settings(OSCAR_ALLOW_ANON_CHECKOUT=True):
            data = self.get_data()
            data["shipping_address"]["phone_number"] = "0612345678"
            ser = serializers.CheckoutSerializer(
                data=data, context={"request": self.get_request(AnonymousUser())}
            )
            self.assertTrue(ser.is_valid())

    @mock.patch.object(ocyan.core.fender.config, "settings", CHECKOUTAPI_OCYAN_OPTIONS)
    def test_wrong_localized_nl_phone_number(self):
        self.test_fill_basket()
        with self.settings(OSCAR_ALLOW_ANON_CHECKOUT=True):
            data = self.get_data()
            data["shipping_address"]["phone_number"] = "0992345678"
            ser = serializers.CheckoutSerializer(
                data=data, context={"request": self.get_request(AnonymousUser())}
            )
            self.assertFalse(ser.is_valid())

    @mock.patch.object(ocyan.core.fender.config, "settings", CHECKOUTAPI_OCYAN_OPTIONS)
    def test_localized_fi_phone_number(self):
        self.test_fill_basket()

        with self.settings(OSCAR_ALLOW_ANON_CHECKOUT=True):
            data = self.get_data()
            data["shipping_address"]["country"] = (
                "http://testserver/checkout/api/countries/FI/"
            )
            data["shipping_address"]["phone_number"] = "0912345678"
            ser = serializers.CheckoutSerializer(
                data=data, context={"request": self.get_request(AnonymousUser())}
            )
            self.assertTrue(ser.is_valid())

    @mock.patch.object(ocyan.core.fender.config, "settings", CHECKOUTAPI_OCYAN_OPTIONS)
    def test_localized_wrong_fi_phone_number(self):
        self.test_fill_basket()

        with self.settings(OSCAR_ALLOW_ANON_CHECKOUT=True):
            data = self.get_data()
            data["shipping_address"]["country"] = (
                "http://testserver/checkout/api/countries/FI/"
            )
            data["shipping_address"]["phone_number"] = "264611234567"
            ser = serializers.CheckoutSerializer(
                data=data, context={"request": self.get_request(AnonymousUser())}
            )
            self.assertFalse(ser.is_valid())

    @mock.patch.object(ocyan.core.fender.config, "settings", CHECKOUTAPI_OCYAN_OPTIONS)
    def test_phone_number_field_still_works(self):
        self.test_fill_basket()

        with self.settings(OSCAR_ALLOW_ANON_CHECKOUT=True):
            data = self.get_data()
            data["shipping_address"]["phone_number"] = PhoneNumber(
                country_code=31,
                national_number=624352007,
                extension=None,
                italian_leading_zero=None,
                number_of_leading_zeros=None,
                country_code_source=20,
                preferred_domestic_carrier_code=None,
            )
            ser = serializers.CheckoutSerializer(
                data=data, context={"request": self.get_request(AnonymousUser())}
            )
            self.assertTrue(ser.is_valid())

    @mock.patch.object(ocyan.core.fender.config, "settings", CHECKOUTAPI_OCYAN_OPTIONS)
    def test_bare_minimum_data(self):
        self.test_fill_basket()

        with self.settings(OSCAR_ALLOW_ANON_CHECKOUT=True):
            data = self.get_bare_data()
            ser = serializers.CheckoutSerializer(
                data=data,
                context={"request": self.get_request(AnonymousUser())},
                partial=True,
            )
            self.assertTrue(ser.is_valid())


@mock.patch.object(ocyan.core.fender.config, "settings", CHECKOUTAPI_OCYAN_OPTIONS)
class TestPaymentMethodView(CheckoutApiTest):
    fixtures = [
        "checkout/auth",
        "checkout/catalogue",
        "checkout/shipping",
        "checkout/paymentmethod",
    ]

    def test_payment_method(self):
        self.fill_basket()
        url = reverse("checkoutapi:api-basket-payment-methods")
        response = self.client.get(url)
        self.assertListEqual(
            response.json(),
            [["dummy:payment", "Dummy betaling"]],
            "The dummy payment should be available.",
        )

        url = reverse("checkoutapi:api-basket-total")
        response = self.client.post(url, content_type="application/json")
        data = response.json()
        self.assertEqual(
            data["total"]["incl_tax"], "305.9500", "The total price should be 305.95."
        )

    def test_payment_method_minimum(self):
        pm = PaymentMethod.objects.get()
        pm.min_amount = D(100)
        pm.save()
        self.test_payment_method()

        pm.min_amount = D(400)
        pm.save()
        url = reverse("checkoutapi:api-basket-payment-methods")
        response = self.client.get(url)
        self.assertListEqual(
            response.json(),
            [],
            "The dummy payment should not be available, because the minimum amount restriction is not met.",
        )

    def test_payment_method_maximum(self):
        pm = PaymentMethod.objects.get()
        pm.max_amount = D(400)
        pm.save()
        self.test_payment_method()

        pm.max_amount = D(100)
        pm.save()
        url = reverse("checkoutapi:api-basket-payment-methods")
        response = self.client.get(url)
        self.assertListEqual(
            response.json(),
            [],
            "The dummy payment should not be available, because the maximum amount restriction is not met.",
        )

    def test_payment_method_maximum_and_minimum(self):
        pm = PaymentMethod.objects.get()
        pm.max_amount = D(400)
        pm.min_amount = D(100)
        pm.save()
        self.test_payment_method()

        pm.min_amount = D(320)
        pm.max_amount = D(350)
        pm.save()
        url = reverse("checkoutapi:api-basket-payment-methods")
        response = self.client.get(url)
        self.assertListEqual(
            response.json(),
            [],
            "The dummy payment should not be available, because the minimum amount restriction is not met.",
        )


@mock.patch.object(ocyan.core.fender.config, "settings", CHECKOUTAPI_OCYAN_OPTIONS)
class TestOrderTotalView(CheckoutApiTest):
    fixtures = [
        "checkout/auth",
        "checkout/catalogue",
        "checkout/shipping",
        "checkout/paymentmethod",
    ]

    def test_no_basket(self):
        data = {
            "guest_email": "cura6b@gmail.com",
            "payment_method": "dummy:payment",
            "shipping_address": {
                "country": "http://testserver/checkout/api/countries/NL/",
                "first_name": "Rob",
                "house_number": "229-123",
                "last_name": "van den Berg",
                "line1": "Nieuwe Veenendaalseweg",
                "line4": "Rhenen",
                "phone_number": "0620306524",
                "postcode": "3911MJ",
                "title": "Mr",
            },
            "shipping_method_code": "postnl",
        }
        url = reverse("checkoutapi:api-basket-total")
        response = self.client.post(
            url, json=data, content_type="application/json; charset=UTF-8"
        )
        self.assertEqual(
            response.status_code,
            406,
            "Order total should not be available for requests with an unsaved basket",
        )
