from decimal import Decimal
from unittest import mock

from django.conf import settings
from django.contrib.auth import get_user_model
from django.test import TestCase
from django.urls import reverse
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_encode
from django.utils.translation import gettext

from oscar.core.loading import get_model

import ocyan.core.fender
from ocyan.core.utils import merge_dicts
from ocyan.main.utils import randomtokengenerator
from ocyan.plugin.oscar.pipeline import PIPELINE_STATUS_NEW

from ..checkout.models import FixedSurcharge

User = get_user_model()
Order = get_model("order", "Order")
Surcharge = get_model("order", "Surcharge")
Country = get_model("address", "Country")
PaymentMethod = get_model("checkout", "PaymentMethod")
FixedSurcharge = get_model("checkout", "FixedSurcharge")
PercentageSurcharge = get_model("checkout", "PercentageSurcharge")

ocyan_settings = ocyan.core.fender.config.settings
CHECKOUT_OCYAN_OPTIONS = merge_dicts(
    ocyan_settings,
    {"shipping": {"enable_charged_shipping": True, "enable_free_shipping": True}},
)


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

    @mock.patch.object(ocyan.core.fender.config, "settings", CHECKOUT_OCYAN_OPTIONS)
    def test_no_problems_just_an_empty_basket(self):
        "No errors have occurred during ordering the user just has an empty basket"
        if not settings.OSCAR_ALLOW_ANON_CHECKOUT:
            henk = User.objects.get(username="henk")
            self.client.force_login(henk)

        self.client.get(reverse("basket:summary"))

        response = self.client.get(reverse("checkout:index"), follow=True)
        self.assertTemplateNotUsed(
            response,
            "checkout/try_again.html",
            msg_prefix="no redirect should have happened to the try again view",
        )
        self.assertTemplateUsed(
            response,
            "checkout/onestep.html",
            msg_prefix="The onestep checkout template should have been rendered",
        )
        self.assertContains(
            response,
            "<h1>%s</h1>" % gettext("Your shopping basket appears to be empty"),
            msg_prefix="Standard message should be displayed",
        )

    @mock.patch.object(ocyan.core.fender.config, "settings", CHECKOUT_OCYAN_OPTIONS)
    def test_error_checkout_visited_with_unfinished_order_in_session(self):
        "When a payment fails, the user should be redirected to the try again page"
        if not settings.OSCAR_ALLOW_ANON_CHECKOUT:
            henk = User.objects.get(username="henk")
            self.client.force_login(henk)

        self.client.get(reverse("basket:summary"))

        s = self.client.session
        s["checkout_order_id"] = 1
        s.save()

        response = self.client.get(reverse("checkout:index"), follow=True)
        self.assertTemplateUsed(
            response,
            "checkout/try_again.html",
            msg_prefix="we should be redirected to the try again page",
        )
        self.assertTemplateNotUsed(
            response,
            "checkout/onestep.html",
            msg_prefix="the onstep html should not be rendered",
        )
        self.assertNotContains(
            response,
            "<h1>%s</h1>" % gettext("Your shopping basket appears to be empty"),
            msg_prefix="No standard error message should be displayed",
        )
        self.assertRedirects(
            response,
            reverse("checkout:try-again"),
            status_code=302,
            target_status_code=200,
            fetch_redirect_response=True,
        )

    @mock.patch.object(ocyan.core.fender.config, "settings", CHECKOUT_OCYAN_OPTIONS)
    def test_error_checkout_visited_with_bugged_order_in_session(self):
        "When a connection error happend during checkout the user should be lead to the try again page"
        o = Order.objects.get()
        o.status = PIPELINE_STATUS_NEW
        o.save()

        if not settings.OSCAR_ALLOW_ANON_CHECKOUT:
            henk = User.objects.get(username="henk")
            self.client.force_login(henk)

        self.client.get(reverse("basket:summary"))

        s = self.client.session
        s["checkout_order_id"] = 1
        s.save()

        response = self.client.get(reverse("checkout:index"), follow=True)
        self.assertTemplateUsed(
            response,
            "checkout/try_again.html",
            msg_prefix="we should be redirected to the try again page",
        )
        self.assertTemplateNotUsed(
            response,
            "checkout/onestep.html",
            msg_prefix="the onstep html should not be rendered",
        )
        self.assertNotContains(
            response,
            "<h1>%s</h1>" % gettext("Your shopping basket appears to be empty"),
            msg_prefix="No standard error message should be displayed",
        )
        self.assertRedirects(
            response,
            reverse("checkout:try-again"),
            status_code=302,
            target_status_code=200,
            fetch_redirect_response=True,
        )

    @mock.patch.object(ocyan.core.fender.config, "settings", CHECKOUT_OCYAN_OPTIONS)
    def test_error_checkout_post_with_unfinished_order_in_session(self):
        "When a payment fails, the user should be redirected to the try again page"
        if not settings.OSCAR_ALLOW_ANON_CHECKOUT:
            henk = User.objects.get(username="henk")
            self.client.force_login(henk)

        self.client.get(reverse("basket:summary"))

        s = self.client.session
        s["checkout_order_id"] = 1
        s.save()

        response = self.client.post(
            reverse("checkout:index"),
            {"payment_method_form-payment_method": "dummy:payment"},
            follow=True,
        )
        self.assertTemplateUsed(
            response,
            "checkout/try_again.html",
            msg_prefix="we should be redirected to the try again page",
        )
        self.assertTemplateNotUsed(
            response,
            "checkout/onestep.html",
            msg_prefix="the onstep html should not be rendered",
        )

        self.assertNotContains(
            response,
            "<h1>%s</h1>" % gettext("Your shopping basket appears to be empty"),
            msg_prefix="No standard error message should be displayed",
        )
        self.assertRedirects(
            response,
            reverse("checkout:try-again"),
            status_code=302,
            target_status_code=200,
            fetch_redirect_response=True,
        )

    @mock.patch.object(ocyan.core.fender.config, "settings", CHECKOUT_OCYAN_OPTIONS)
    def test_error_checkout_post_with_bugged_order_in_session(self):
        "When a connection error happend during checkout the user should be lead to the try again page"
        o = Order.objects.get()
        o.status = PIPELINE_STATUS_NEW
        o.save()

        if not settings.OSCAR_ALLOW_ANON_CHECKOUT:
            henk = User.objects.get(username="henk")
            self.client.force_login(henk)

        self.client.get(reverse("basket:summary"))

        s = self.client.session
        s["checkout_order_id"] = 1
        s.save()

        # as long as the payment method is post everything should be fine
        response = self.client.post(
            reverse("checkout:index"),
            {"payment_method_form-payment_method": "dummy:payment"},
            follow=True,
        )

        self.assertTemplateUsed(
            response,
            "payment/dummy/payment.html",
            msg_prefix="we should be redirected to the payment page",
        )
        self.assertTemplateNotUsed(
            response,
            "checkout/try_again.html",
            msg_prefix="we should not be redirected to the try again page",
        )
        self.assertTemplateNotUsed(
            response,
            "checkout/onestep.html",
            msg_prefix="the onstep html should not be rendered",
        )
        self.assertNotContains(
            response,
            "<h1>%s</h1>" % gettext("Your shopping basket appears to be empty"),
            msg_prefix="No standard error message should be displayed",
        )
        self.assertRedirects(
            response,
            reverse("dummy:payment"),
            status_code=302,
            target_status_code=200,
            fetch_redirect_response=True,
        )

    def test_tryagain_from_link(self):
        "try again from link should work"
        # make henk logged in
        henk = User.objects.get(username="henk")
        self.client.force_login(henk)

        order = Order.objects.get(pk=1)
        order.user = henk
        order.save()
        token = randomtokengenerator.make_token(order.user)
        base64_order_id = urlsafe_base64_encode(force_bytes(order.pk))

        url = reverse(
            "checkout:try-again-from-link",
            kwargs={"oidb64": base64_order_id, "token": token},
        )
        self.assertTrue(randomtokengenerator.check_token(henk, token))

        response = self.client.get(url)
        self.assertEqual(response.status_code, 200)

    def test_hack_tryagain_from_link(self):
        "trying again from link of somebody else does not work"
        # make henk logged in
        henk = User.objects.get(username="henk")
        self.client.force_login(henk)

        order = Order.objects.get(pk=1)
        order.user = henk
        order.save()
        token = randomtokengenerator.make_token(order.user)
        base64_order_id = urlsafe_base64_encode(force_bytes(order.pk))

        url = reverse(
            "checkout:try-again-from-link",
            kwargs={"oidb64": base64_order_id, "token": token},
        )
        self.assertTrue(randomtokengenerator.check_token(henk, token))

        henk = User.objects.get(username="admin")
        self.client.force_login(henk)

        response = self.client.get(url)
        self.assertEqual(response.status_code, 404)

    def test_try_again_anonymous(self):
        "Anonymous users should be able to pay their open order"
        order = Order.objects.get(pk=1)
        token = order.verification_hash()
        base64_order_id = urlsafe_base64_encode(force_bytes(order.pk))

        url = reverse(
            "checkout:try-again-from-link",
            kwargs={"oidb64": base64_order_id, "token": token},
        )
        self.assertTrue(order.check_verification_hash(token))

        response = self.client.get(url)
        self.assertEqual(response.status_code, 200)

    def test_hack_again_anonymous(self):
        "Anonymous hackers should not be able to pay their open order"
        order = Order.objects.get(pk=1)
        token = "5ul-cc8bf994d7002701c70e"
        base64_order_id = urlsafe_base64_encode(force_bytes(order.pk))

        url = reverse(
            "checkout:try-again-from-link",
            kwargs={"oidb64": base64_order_id, "token": token},
        )
        self.assertFalse(order.check_verification_hash(token))

        response = self.client.get(url)
        self.assertEqual(response.status_code, 404)

    def test_try_again_on_order_with_surcharges(self):
        """
        When an order has surcharges, the try again should on initial page load re-calculate
        these surcharges. As the customer is able to choose a different payment method, scenarios
        can have changed since the order was created (eg new surcharges added, or surcharges removed).

        The try again does the following things;
        - re-calculate the surcharges
        - re-calculate the order total
        - upon POST, the actual totals will also be saved (initially (GET) it's just displayed).

        """
        order = Order.objects.get(pk=1)
        token = order.verification_hash()
        base64_order_id = urlsafe_base64_encode(force_bytes(order.pk))

        # add a surcharge to the order
        order_surcharge = order.surcharges.create(
            name="test",
            code="test",
            incl_tax=Decimal("10.00"),
            excl_tax=Decimal("8.26"),
        )
        order.total_incl_tax += order_surcharge.incl_tax
        order.total_excl_tax += order_surcharge.excl_tax
        order.save()

        # Ensure the order totals are as expected (with surcharge included)
        self.assertEqual(order.total_incl_tax, Decimal("141.95"))
        self.assertEqual(order.total_excl_tax, Decimal("117.31"))

        url = reverse(
            "checkout:try-again-from-link",
            kwargs={"oidb64": base64_order_id, "token": token},
        )

        # Surcharges should be empty on initial page load.
        response = self.client.get(url)
        self.assertEqual(response.context["surcharges"], None)

        # Assert order totals from response.context["order"], this should not include the surcharge
        self.assertEqual(response.context["order"].total_incl_tax, Decimal("131.95"))
        self.assertEqual(response.context["order"].total_excl_tax, Decimal("109.05"))

        # Now POST the form, there are no surcharges yet for dummy payment, so the order total should be the same
        response = self.client.post(
            url,
            {"_try-again": "try-again", "payment_method": "dummy:payment"},
            follow=True,
        )
        order.refresh_from_db()
        self.assertEqual(order.total_incl_tax, Decimal("131.95"))
        self.assertEqual(order.total_excl_tax, Decimal("109.05"))

        # Create a fixed surcharge for the dummy payment method and try again.
        fixed_charge = FixedSurcharge.objects.create(
            name="Dummy payment surcharge",
            code="dummy-payment-surcharge",
            price=Decimal("500.00"),
        )
        fixed_charge.payment_method.set(
            PaymentMethod.objects.filter(key="dummy:payment")
        )
        fixed_charge.country.set(Country.objects.all())

        # Now POST the form, there are surcharges for dummy payment, so the order total should be updated
        response = self.client.post(
            url,
            {"_try-again": "try-again", "payment_method": "dummy:payment"},
            follow=True,
        )
        order.refresh_from_db()
        self.assertEqual(order.total_incl_tax, Decimal("631.95"))
        self.assertEqual(order.total_excl_tax, Decimal("522.27"))

        # Create a second surcharge for the dummy payment method and try again.
        percentage_surcharge = PercentageSurcharge.objects.create(
            name="Dummy payment percentage surcharge",
            code="dummy-payment-percentage-surcharge",
            percentage=Decimal("10.00"),
            include_shipping_charges=True,
        )
        percentage_surcharge.payment_method.set(
            PaymentMethod.objects.filter(key="dummy:payment")
        )
        percentage_surcharge.country.set(Country.objects.all())

        # Now POST the form, there are multiple surcharges for dummy payment, so the order total should be updated
        response = self.client.post(
            url,
            {"_try-again": "try-again", "payment_method": "dummy:payment"},
            follow=True,
        )
        order.refresh_from_db()
        # We created a percentage charge that INCLDUES shipping charges.
        # This means the percentage is calculated over: basket.total_incl_tax + shipping_charge.incl_tax
        # The shipping charge is 6.95 (incl), so the percentage is calculated over 125.00 + 6.95 = 131.95
        # The percentage is 10%, so the surcharge is 13.195 (rounded to 13.20)
        # The total should be 125.00 + 6.95 + 500 (fixed surcharge) + 13.20 (percentage surcharge) = 645.15
        self.assertEqual(order.total_incl_tax, Decimal("645.15"))
        self.assertEqual(order.total_excl_tax, Decimal("533.18"))

    def test_try_again_render_totals(self):
        """Test the render_totals handler for Javascript request"""
        order = Order.objects.get(pk=1)
        token = order.verification_hash()
        base64_order_id = urlsafe_base64_encode(force_bytes(order.pk))

        url = reverse(
            "checkout:try-again-from-link",
            kwargs={"oidb64": base64_order_id, "token": token},
        )

        # Simulate AJAX request
        response = self.client.get(
            url + "?payment_method=dummy:payment",
            HTTP_X_REQUESTED_WITH="XMLHttpRequest",
        )
        self.assertEqual(response.status_code, 200)
        self.assertTemplateUsed(
            response, "checkout/partials/try_again_totals_block.html"
        )
        self.assertTemplateNotUsed(response, "checkout/try_again.html")

        self.assertEqual(response.context["order"].total_incl_tax, Decimal("131.95"))
        self.assertEqual(response.context["order"].total_excl_tax, Decimal("109.05"))

        # Create a fixed surcharge for the dummy payment method and try again.
        fixed_charge = FixedSurcharge.objects.create(
            name="Dummy payment surcharge",
            code="dummy-payment-surcharge",
            price=Decimal("500.00"),
        )
        fixed_charge.payment_method.set(
            PaymentMethod.objects.filter(key="dummy:payment")
        )
        fixed_charge.country.set(Country.objects.all())

        # Simulate AJAX request
        response = self.client.get(
            url + "?payment_method=dummy:payment",
            HTTP_X_REQUESTED_WITH="XMLHttpRequest",
        )
        self.assertEqual(response.context["order"].total_incl_tax, Decimal("631.95"))
        self.assertEqual(response.context["order"].total_excl_tax, Decimal("522.2731"))

        # Create a second surcharge for the dummy payment method and try again.
        percentage_surcharge = PercentageSurcharge.objects.create(
            name="Dummy payment percentage surcharge",
            code="dummy-payment-percentage-surcharge",
            percentage=Decimal("10.00"),
            include_shipping_charges=False,
        )
        percentage_surcharge.payment_method.set(
            PaymentMethod.objects.filter(key="dummy:payment")
        )
        percentage_surcharge.country.set(Country.objects.all())

        # Now POST the form, there are multiple surcharges for dummy payment, so the order total should be updated
        response = self.client.get(
            url + "?payment_method=dummy:payment",
            HTTP_X_REQUESTED_WITH="XMLHttpRequest",
        )
        # We created a percentage charge that EXCLUDES shipping charges.
        # This means the percentage is calculated over: basket.total_incl_tax only
        # The base amount is 125.00
        # The percentage is 10%, so the surcharge is 12.50
        # The shipping charge is 6.95 (incl)
        # The total should be 125.00 + 6.95 + 500 (fixed surcharge) + 12.50 (percentage surcharge) = 644.45
        self.assertEqual(response.context["order"].total_incl_tax, Decimal("644.45"))
        self.assertEqual(response.context["order"].total_excl_tax, Decimal("532.6037"))
