# -*- coding: utf-8 -*- pylint: disable=E1120
from decimal import Decimal as D
from unittest import mock

from django.contrib.auth import get_user_model
from django.test import RequestFactory, TestCase, override_settings
from django.urls import reverse

from oscar.core.loading import get_model

from oscarapi.basket.operations import get_user_basket, prepare_basket

import ocyan.core.fender
import ocyan.plugin.oscar.taxes.taxrate
from ocyan.plugin.oscar.taxes.utils import (
    exclusive_tax_component,
    tax_from_price_incl_tax,
)

Basket = get_model("basket", "Basket")
Product = get_model("catalogue", "Product")
User = get_user_model()
factory = RequestFactory()

OCYAN_MOCK_CONFIG_INCL_TAX = {"oscar": {"use_price_incl_tax": True}}
OCYAN_MOCK_CONFIG_EXCL_TAX = {"oscar": {"use_price_incl_tax": False}}


# Mocking method
def get_tax_rate_as_fraction_mocked_defaults():
    """
    All tests below are tested with the NL vat country in mind. Projects having a different value
    set will have failing tests, before this mock we would have added those tests in the skipped_tests list.
    Instead of skipping the tests, it's better to just mock the value to NL, as there are tests in other plugins
    that test with different OSCAR_VAT_COUNTRY_OF_ORIGIN_CODE.

    Django's override_settings does not work when a setting is used as default method argument, eg;
    def foo(default=settings.SOME_SETIING)

    So that's why we're mocking this default argument with mock.patch.
    """
    defaults = list(ocyan.plugin.oscar.taxes.taxrate.tax_rate_as_fraction.__defaults__)
    defaults[0] = "NL"
    return tuple(defaults)


tax_rate_as_fraction_mocked_defaults = get_tax_rate_as_fraction_mocked_defaults()


@override_settings(OSCAR_VAT_COUNTRY_OF_ORIGIN_CODE="NL")
@mock.patch.object(
    ocyan.plugin.oscar.taxes.taxrate.tax_rate_as_fraction,
    "__defaults__",
    tax_rate_as_fraction_mocked_defaults,
)
class TaxTest(TestCase):
    fixtures = [
        "catalogue/catalogue",
        "catalogue/auth",
    ]

    def assertLessThanOneCentDifference(self, calculated, basket):
        self.assertLessEqual(abs(calculated - basket), D("0.01"))

    def test_catalogue_initialized(self):
        "The catalogue should be initialized"
        self.assertEqual(Product.objects.count(), 4)
        self.assertEqual(User.objects.count(), 2)

    @mock.patch.object(ocyan.core.fender.config, "settings", OCYAN_MOCK_CONFIG_INCL_TAX)
    def test_add_product_to_basket_incl_tax(self):
        admin = User.objects.get(username="admin")
        self.client.force_login(admin)
        basket = get_user_basket(admin)
        self.assertEqual(Basket.objects.count(), 1)

        self.assertEqual(basket.total_excl_tax_excl_discounts, D("0.00"))
        self.assertEqual(basket.total_incl_tax_excl_discounts, D("0.00"))
        self.assertEqual(basket.total_discount, D("0.00"))
        self.assertEqual(basket.total_excl_tax, D("0.00"))
        self.assertEqual(basket.total_tax, D("0.00"))
        self.assertEqual(basket.total_incl_tax, D("0.00"))

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

        response = self.client.post(basket_add_url, body)

        request = factory.post(basket_add_url, body)
        request.user = admin
        request.session = self.client.session

        basket.refresh_from_db()
        basket = prepare_basket(basket, request)

        self.assertEqual(response.status_code, 302)
        self.assertEqual(Basket.objects.count(), 1)

        self.assertEqual(basket.total_excl_tax_excl_discounts, D("330.58"))
        self.assertEqual(basket.total_incl_tax_excl_discounts, D("400.00"))
        self.assertEqual(basket.total_discount, D("0.00"))
        self.assertEqual(basket.total_excl_tax, D("330.58"))
        self.assertEqual(basket.total_tax, D("69.42"))
        self.assertEqual(basket.total_incl_tax, D("400.00"))
        self.assertEqual(
            tax_from_price_incl_tax(basket.total_incl_tax, D(".21")), D("69.4215")
        )
        self.assertAlmostEqual(
            exclusive_tax_component(D("0.21")) * basket.total_excl_tax,
            basket.total_incl_tax,
            2,
        )

    @mock.patch.object(ocyan.core.fender.config, "settings", OCYAN_MOCK_CONFIG_INCL_TAX)
    def test_add_multiple_products_to_the_freaking_basket_incl_tax(self):
        "Adding mutltiple products should still calculate tax correctly"
        self.test_add_product_to_basket_incl_tax()

        admin = User.objects.get(username="admin")
        basket = get_user_basket(admin)

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

        response = self.client.post(basket_add_url, body)

        request = factory.post(basket_add_url, body)
        request.user = admin
        request.session = self.client.session

        basket.refresh_from_db()
        basket = prepare_basket(basket, request)

        self.assertEqual(response.status_code, 302)
        self.assertEqual(Basket.objects.count(), 1)

        self.assertEqual(basket.total_excl_tax_excl_discounts, D("368.60"))
        self.assertEqual(basket.total_incl_tax_excl_discounts, D("446.00"))
        self.assertEqual(basket.total_discount, D("0.00"))
        self.assertEqual(basket.total_excl_tax, D("368.60"))
        self.assertEqual(basket.total_tax, D("77.40"))
        self.assertEqual(basket.total_incl_tax, D("446.00"))
        self.assertLessThanOneCentDifference(
            exclusive_tax_component(D("0.21")) * basket.total_excl_tax,
            basket.total_incl_tax,
        )

    @mock.patch.object(ocyan.core.fender.config, "settings", OCYAN_MOCK_CONFIG_EXCL_TAX)
    def test_add_product_to_basket_excl_tax(self):
        admin = User.objects.get(username="admin")
        self.client.force_login(admin)
        basket = get_user_basket(admin)
        self.assertEqual(Basket.objects.count(), 1)

        self.assertEqual(basket.total_excl_tax_excl_discounts, D("0.00"))
        self.assertEqual(basket.total_incl_tax_excl_discounts, D("0.00"))
        self.assertEqual(basket.total_discount, D("0.00"))
        self.assertEqual(basket.total_excl_tax, D("0.00"))
        self.assertEqual(basket.total_tax, D("0.00"))
        self.assertEqual(basket.total_incl_tax, D("0.00"))

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

        response = self.client.post(basket_add_url, body)

        request = factory.post(basket_add_url, body)
        request.user = admin
        request.session = self.client.session

        basket.refresh_from_db()
        basket = prepare_basket(basket, request)

        self.assertEqual(response.status_code, 302)
        self.assertEqual(Basket.objects.count(), 1)

        self.assertEqual(basket.total_excl_tax_excl_discounts, D("400.00"))
        self.assertEqual(basket.total_incl_tax_excl_discounts, D("484.00"))
        self.assertEqual(basket.total_discount, D("0.00"))
        self.assertEqual(basket.total_excl_tax, D("400.00"))
        self.assertEqual(basket.total_tax, D("84.00"))
        self.assertEqual(basket.total_incl_tax, D("484.00"))
        self.assertEqual(
            tax_from_price_incl_tax(basket.total_incl_tax, D(".21")), basket.total_tax
        )
        self.assertAlmostEqual(
            exclusive_tax_component(D("0.21")) * basket.total_excl_tax,
            basket.total_incl_tax,
            2,
        )

    @mock.patch.object(ocyan.core.fender.config, "settings", OCYAN_MOCK_CONFIG_EXCL_TAX)
    def test_add_multiple_products_to_the_freaking_basket_excl_tax(self):
        self.test_add_product_to_basket_excl_tax()

        admin = User.objects.get(username="admin")
        basket = get_user_basket(admin)

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

        response = self.client.post(basket_add_url, body)

        request = factory.post(basket_add_url, body)
        request.user = admin
        request.session = self.client.session

        basket.refresh_from_db()
        basket = prepare_basket(basket, request)

        self.assertEqual(response.status_code, 302)
        self.assertEqual(Basket.objects.count(), 1)

        self.assertEqual(basket.total_excl_tax_excl_discounts, D("446.00"))
        self.assertEqual(basket.total_incl_tax_excl_discounts, D("539.66"))
        self.assertEqual(basket.total_discount, D("0.00"))
        self.assertEqual(basket.total_excl_tax, D("446.00"))
        self.assertEqual(basket.total_tax, D("93.66"))
        self.assertEqual(basket.total_incl_tax, D("539.66"))
        self.assertLessThanOneCentDifference(
            exclusive_tax_component(D("0.21")) * basket.total_excl_tax,
            basket.total_incl_tax,
        )


@override_settings(OSCAR_VAT_COUNTRY_OF_ORIGIN_CODE="NL")
@mock.patch.object(
    ocyan.plugin.oscar.taxes.taxrate.tax_rate_as_fraction,
    "__defaults__",
    tax_rate_as_fraction_mocked_defaults,
)
class OfferTest(TaxTest):
    fixtures = TaxTest.fixtures + ["catalogue/offer"]

    @mock.patch.object(ocyan.core.fender.config, "settings", OCYAN_MOCK_CONFIG_INCL_TAX)
    def test_add_multiple_products_to_the_freaking_basket_incl_tax(self):
        with self.settings(OSCAR_OFFERS_INCL_TAX=True):
            self.test_add_product_to_basket_incl_tax()

            admin = User.objects.get(username="admin")
            basket = get_user_basket(admin)

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

            response = self.client.post(basket_add_url, body)

            request = factory.post(basket_add_url, body)
            request.user = admin
            request.session = self.client.session

            basket.refresh_from_db()
            basket = prepare_basket(basket, request)

            self.assertEqual(response.status_code, 302)
            self.assertEqual(Basket.objects.count(), 1)

            self.assertEqual(basket.total_excl_tax_excl_discounts, D("368.60"))
            self.assertEqual(basket.total_incl_tax_excl_discounts, D("446.00"))
            self.assertEqual(basket.total_discount, D("4.60"))
            self.assertEqual(basket.total_excl_tax, D("364.79"))
            self.assertEqual(basket.total_tax, D("76.61"))
            self.assertEqual(basket.total_incl_tax, D("441.40"))
            self.assertLessThanOneCentDifference(
                exclusive_tax_component(D("0.21")) * basket.total_excl_tax,
                basket.total_incl_tax,
            )

    @mock.patch.object(ocyan.core.fender.config, "settings", OCYAN_MOCK_CONFIG_EXCL_TAX)
    def test_add_multiple_products_to_the_freaking_basket_excl_tax(self):
        with self.settings(OSCAR_OFFERS_INCL_TAX=False):
            self.test_add_product_to_basket_excl_tax()

            admin = User.objects.get(username="admin")
            basket = get_user_basket(admin)

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

            response = self.client.post(basket_add_url, body)

            request = factory.post(basket_add_url, body)
            request.user = admin
            request.session = self.client.session

            basket.refresh_from_db()
            basket = prepare_basket(basket, request)

            self.assertEqual(response.status_code, 302)
            self.assertEqual(Basket.objects.count(), 1)

            self.assertEqual(basket.total_excl_tax_excl_discounts, D("446.00"))
            self.assertEqual(basket.total_incl_tax_excl_discounts, D("539.66"))
            self.assertEqual(basket.total_discount, D("4.60"))
            self.assertEqual(basket.total_excl_tax, D("441.40"))
            self.assertEqual(basket.total_tax, D("92.69"))
            self.assertEqual(basket.total_incl_tax, D("534.09"))

            self.assertAlmostEqual(
                exclusive_tax_component(D("0.21")) * basket.total_excl_tax,
                basket.total_incl_tax,
                2,
            )

    @mock.patch.object(ocyan.core.fender.config, "settings", OCYAN_MOCK_CONFIG_EXCL_TAX)
    def test_add_product_to_basket_with_flat_discount_excl_tax(self):
        with self.settings(OSCAR_OFFERS_INCL_TAX=False):
            admin = User.objects.get(username="admin")
            self.client.force_login(admin)
            basket = get_user_basket(admin)
            self.assertEqual(Basket.objects.count(), 1)

            self.assertEqual(basket.total_excl_tax_excl_discounts, D("0.00"))
            self.assertEqual(basket.total_incl_tax_excl_discounts, D("0.00"))
            self.assertEqual(basket.total_discount, D("0.00"))
            self.assertEqual(basket.total_excl_tax, D("0.00"))
            self.assertEqual(basket.total_tax, D("0.00"))
            self.assertEqual(basket.total_incl_tax, D("0.00"))

            basket_add_url = reverse("basket:add", args=(4,))
            body = {"quantity": 2}

            response = self.client.post(basket_add_url, body)

            request = factory.post(basket_add_url, body)
            request.user = admin
            request.session = self.client.session

            basket.refresh_from_db()
            basket = prepare_basket(basket, request)

            self.assertEqual(response.status_code, 302)
            self.assertEqual(Basket.objects.count(), 1)

            self.assertEqual(basket.total_excl_tax_excl_discounts, D("468.00"))
            self.assertEqual(basket.total_incl_tax_excl_discounts, D("566.28"))

            self.assertEqual(basket.total_discount, D("10.00"))
            self.assertEqual(basket.total_tax, D("96.18"))
            self.assertEqual(basket.total_incl_tax, D("554.18"))
            self.assertEqual(basket.total_excl_tax, D("458.00"))
            self.assertEqual(
                tax_from_price_incl_tax(basket.total_incl_tax, D(".21")),
                basket.total_tax,
            )
            self.assertLessThanOneCentDifference(
                exclusive_tax_component(D("0.21")) * basket.total_excl_tax,
                basket.total_incl_tax,
            )

    @mock.patch.object(ocyan.core.fender.config, "settings", OCYAN_MOCK_CONFIG_INCL_TAX)
    def test_add_product_to_basket_with_flat_discount_incl_tax(self):
        with self.settings(OSCAR_OFFERS_INCL_TAX=True):
            admin = User.objects.get(username="admin")
            self.client.force_login(admin)
            basket = get_user_basket(admin)
            self.assertEqual(Basket.objects.count(), 1)

            self.assertEqual(basket.total_excl_tax_excl_discounts, D("0.00"))
            self.assertEqual(basket.total_incl_tax_excl_discounts, D("0.00"))
            self.assertEqual(basket.total_discount, D("0.00"))
            self.assertEqual(basket.total_excl_tax, D("0.00"))
            self.assertEqual(basket.total_tax, D("0.00"))
            self.assertEqual(basket.total_incl_tax, D("0.00"))

            basket_add_url = reverse("basket:add", args=(4,))
            body = {"quantity": 2}

            response = self.client.post(basket_add_url, body)

            request = factory.post(basket_add_url, body)
            request.user = admin
            request.session = self.client.session

            basket.refresh_from_db()
            basket = prepare_basket(basket, request)

            self.assertEqual(response.status_code, 302)
            self.assertEqual(Basket.objects.count(), 1)

            self.assertEqual(basket.total_excl_tax_excl_discounts, D("386.78"))
            self.assertEqual(basket.total_incl_tax_excl_discounts, D("468.00"))
            self.assertEqual(basket.total_discount, D("10.00"))
            self.assertEqual(basket.total_incl_tax, D("458.00"))
            self.assertEqual(basket.total_excl_tax, D("378.51"))
            self.assertEqual(basket.total_tax, D("79.49"))
            self.assertLessThanOneCentDifference(
                tax_from_price_incl_tax(basket.total_incl_tax, D(".21")),
                basket.total_tax,
            )
            self.assertLessThanOneCentDifference(
                exclusive_tax_component(D("0.21")) * basket.total_excl_tax,
                basket.total_incl_tax,
            )


class ProductTest(TestCase):
    fixtures = ["catalogue/catalogue"]

    def test_clean_whitespace(self):
        values = ["<p></p>", " ", "<p> </p>", "<div> <p> </p></div>"]
        product = Product.objects.first()
        for value in values:
            product.description = value
            product.long_description = value
            product.clean()
            self.assertEqual("", product.description)
            self.assertEqual("", product.long_description)

    def test_clean_non_whitespace(self):
        values = [
            "<p>a</p>",
            " a ",
            "<p> a </p>",
            "<div> <p> a </p></div>",
            "<div> <img alt='some image' /> </div>",
        ]
        product = Product.objects.first()
        for value in values:
            product.description = value
            product.long_description = value
            product.clean()
            self.assertEqual(value.strip(), product.description)
            self.assertEqual(value.strip(), product.long_description)
