#
# Copyright 2016,2017
# - Skia <skia@libskia.so>
# - Maréchal <thgirod@hotmail.com
#
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr.
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License a published by the Free Software
# Foundation; either version 3 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 Sofware Foundation, Inc., 59 Temple
# Place - Suite 330, Boston, MA 02111-1307, USA.
#
#
import base64
import json
import urllib
from typing import TYPE_CHECKING

from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15
from cryptography.hazmat.primitives.hashes import SHA1
from cryptography.hazmat.primitives.serialization import load_pem_private_key
from django.conf import settings
from django.db.models import Max
from django.test import TestCase
from django.urls import reverse

from core.models import User
from counter.models import Counter, Customer, Product, Selling
from eboutic.models import Basket, BasketItem

if TYPE_CHECKING:
    from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey


class TestEboutic(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.barbar = Product.objects.get(code="BARB")
        cls.refill = Product.objects.get(code="15REFILL")
        cls.cotis = Product.objects.get(code="1SCOTIZ")
        cls.eboutic = Counter.objects.get(name="Eboutic")
        cls.skia = User.objects.get(username="skia")
        cls.subscriber = User.objects.get(username="subscriber")
        cls.old_subscriber = User.objects.get(username="old_subscriber")
        cls.public = User.objects.get(username="public")

    def get_busy_basket(self, user) -> Basket:
        """Create and return a basket with 3 barbar and 1 cotis in it.

        Edit the client session to store the basket id in it.
        """
        session = self.client.session
        basket = Basket.objects.create(user=user)
        session["basket_id"] = basket.id
        session.save()
        BasketItem.from_product(self.barbar, 3, basket).save()
        BasketItem.from_product(self.cotis, 1, basket).save()
        return basket

    def generate_bank_valid_answer(self) -> str:
        basket = Basket.from_session(self.client.session)
        basket_id = basket.id
        amount = int(basket.total * 100)
        query = f"Amount={amount}&BasketID={basket_id}&Auto=42&Error=00000"
        with open("./eboutic/tests/private_key.pem", "br") as f:
            PRIVKEY = f.read()
        with open("./eboutic/tests/public_key.pem") as f:
            settings.SITH_EBOUTIC_PUB_KEY = f.read()
        key: RSAPrivateKey = load_pem_private_key(PRIVKEY, None)
        sig = key.sign(query.encode("utf-8"), PKCS1v15(), SHA1())
        b64sig = base64.b64encode(sig).decode("ascii")

        url = reverse("eboutic:etransation_autoanswer") + "?%s&Sig=%s" % (
            query,
            urllib.parse.quote_plus(b64sig),
        )
        return url

    def test_buy_with_sith_account(self):
        self.client.force_login(self.subscriber)
        self.subscriber.customer.amount = 100  # give money before test
        self.subscriber.customer.save()
        basket = self.get_busy_basket(self.subscriber)
        amount = basket.total
        response = self.client.post(reverse("eboutic:pay_with_sith"))
        self.assertRedirects(response, "/eboutic/pay/success/")
        new_balance = Customer.objects.get(user=self.subscriber).amount
        assert float(new_balance) == 100 - amount
        expected = 'basket_items=""; expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=0; Path=/eboutic'
        assert expected == self.client.cookies["basket_items"].OutputString()

    def test_buy_with_sith_account_no_money(self):
        self.client.force_login(self.subscriber)
        basket = self.get_busy_basket(self.subscriber)
        initial = basket.total - 1  # just not enough to complete the sale
        self.subscriber.customer.amount = initial
        self.subscriber.customer.save()
        response = self.client.post(reverse("eboutic:pay_with_sith"))
        self.assertRedirects(response, "/eboutic/pay/failure/")
        new_balance = Customer.objects.get(user=self.subscriber).amount
        assert float(new_balance) == initial
        # this cookie should be removed after payment
        expected = 'basket_items=""; expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=0; Path=/eboutic'
        assert expected == self.client.cookies["basket_items"].OutputString()

    def test_submit_basket(self):
        self.client.force_login(self.subscriber)
        self.client.cookies["basket_items"] = """[
            {"id": 2, "name": "Cotis 2 semestres", "quantity": 1, "unit_price": 28},
            {"id": 4, "name": "Barbar", "quantity": 3, "unit_price": 1.7}
        ]"""
        response = self.client.get(reverse("eboutic:command"))
        assert response.status_code == 200
        self.assertInHTML(
            "<tr><td>Cotis 2 semestres</td><td>1</td><td>28.00 €</td></tr>",
            response.content.decode(),
        )
        self.assertInHTML(
            "<tr><td>Barbar</td><td>3</td><td>1.70 €</td></tr>",
            response.content.decode(),
        )
        assert "basket_id" in self.client.session
        basket = Basket.objects.get(id=self.client.session["basket_id"])
        assert basket.items.count() == 2
        barbar = basket.items.filter(product_name="Barbar").first()
        assert barbar is not None
        assert barbar.quantity == 3
        cotis = basket.items.filter(product_name="Cotis 2 semestres").first()
        assert cotis is not None
        assert cotis.quantity == 1
        assert basket.total == 3 * 1.7 + 28

    def test_submit_empty_basket(self):
        self.client.force_login(self.subscriber)
        self.client.cookies["basket_items"] = "[]"
        response = self.client.get(reverse("eboutic:command"))
        self.assertRedirects(response, "/eboutic/")

    def test_submit_invalid_basket(self):
        self.client.force_login(self.subscriber)
        max_id = Product.objects.aggregate(res=Max("id"))["res"]
        self.client.cookies["basket_items"] = f"""[
            {{"id": {max_id + 1}, "name": "", "quantity": 1, "unit_price": 28}}
        ]"""
        response = self.client.get(reverse("eboutic:command"))
        cookie = self.client.cookies["basket_items"].OutputString()
        assert 'basket_items="[]"' in cookie
        assert "Path=/eboutic" in cookie
        self.assertRedirects(response, "/eboutic/")

    def test_submit_basket_illegal_quantity(self):
        self.client.force_login(self.subscriber)
        self.client.cookies["basket_items"] = """[
            {"id": 4, "name": "Barbar", "quantity": -1, "unit_price": 1.7}
        ]"""
        response = self.client.get(reverse("eboutic:command"))
        self.assertRedirects(response, "/eboutic/")

    def test_buy_subscribe_product_with_credit_card(self):
        self.client.force_login(self.old_subscriber)
        response = self.client.get(
            reverse("core:user_profile", kwargs={"user_id": self.old_subscriber.id})
        )
        assert "Non cotisant" in str(response.content)
        self.client.cookies["basket_items"] = """[
            {"id": 2, "name": "Cotis 2 semestres", "quantity": 1, "unit_price": 28}
        ]"""
        response = self.client.get(reverse("eboutic:command"))
        self.assertInHTML(
            "<tr><td>Cotis 2 semestres</td><td>1</td><td>28.00 €</td></tr>",
            response.content.decode(),
        )
        basket = Basket.objects.get(id=self.client.session["basket_id"])
        assert basket.items.count() == 1
        response = self.client.get(self.generate_bank_valid_answer())
        assert response.status_code == 200
        assert response.content.decode("utf-8") == "Payment successful"

        subscriber = User.objects.get(id=self.old_subscriber.id)
        assert subscriber.subscriptions.count() == 2
        sub = subscriber.subscriptions.order_by("-subscription_end").first()
        assert sub.is_valid_now()
        assert sub.member == subscriber
        assert sub.subscription_type == "deux-semestres"
        assert sub.location == "EBOUTIC"

    def test_buy_refill_product_with_credit_card(self):
        self.client.force_login(self.subscriber)
        # basket contains 1 refill item worth 15€
        self.client.cookies["basket_items"] = json.dumps(
            [{"id": 3, "name": "Rechargement 15 €", "quantity": 1, "unit_price": 15}]
        )
        initial_balance = self.subscriber.customer.amount
        self.client.get(reverse("eboutic:command"))

        url = self.generate_bank_valid_answer()
        response = self.client.get(url)
        assert response.status_code == 200
        assert response.content.decode() == "Payment successful"
        new_balance = Customer.objects.get(user=self.subscriber).amount
        assert new_balance == initial_balance + 15

    def test_alter_basket_after_submission(self):
        self.client.force_login(self.subscriber)
        self.client.cookies["basket_items"] = json.dumps(
            [{"id": 4, "name": "Barbar", "quantity": 1, "unit_price": 1.7}]
        )
        self.client.get(reverse("eboutic:command"))
        et_answer_url = self.generate_bank_valid_answer()
        self.client.cookies["basket_items"] = json.dumps(
            [  # alter basket
                {"id": 4, "name": "Barbar", "quantity": 3, "unit_price": 1.7}
            ]
        )
        self.client.get(reverse("eboutic:command"))
        response = self.client.get(et_answer_url)
        assert response.status_code == 500
        msg = (
            "Basket processing failed with error: "
            "SuspiciousOperation('Basket total and amount do not match'"
        )
        assert msg in response.content.decode("utf-8")

    def test_buy_simple_product_with_credit_card(self):
        self.client.force_login(self.subscriber)
        self.client.cookies["basket_items"] = json.dumps(
            [{"id": 4, "name": "Barbar", "quantity": 1, "unit_price": 1.7}]
        )
        self.client.get(reverse("eboutic:command"))
        et_answer_url = self.generate_bank_valid_answer()
        response = self.client.get(et_answer_url)
        assert response.status_code == 200
        assert response.content.decode("utf-8") == "Payment successful"

        selling = (
            Selling.objects.filter(customer=self.subscriber.customer)
            .order_by("-date")
            .first()
        )
        assert selling.payment_method == "CARD"
        assert selling.quantity == 1
        assert selling.unit_price == self.barbar.selling_price
        assert selling.counter.type == "EBOUTIC"
        assert selling.product == self.barbar