import time
from datetime import datetime, timedelta
from decimal import Decimal

import requests_mock
import simplejson as json
from django.contrib.auth import get_user_model
from django.core.management import call_command
from django.utils import dateformat, timezone
from freezegun import freeze_time
from rest_framework.reverse import reverse

from aa_stripe.forms import StripeCouponForm
from aa_stripe.models import StripeCoupon
from aa_stripe.utils import timestamp_to_timezone_aware_date
from tests.test_utils import BaseTestCase

UserModel = get_user_model()


class TestCoupons(BaseTestCase):
    @freeze_time("2016-01-01 00:00:00")
    def test_create(self):
        # test creating simple coupon with no coupon_id specified (will be generated by Stripe)
        created = int(time.mktime(datetime.now().timetuple()))
        stripe_response = {
            "id": "25OFF",
            "object": "coupon",
            "amount_off": 1,
            "created": created,
            "currency": "usd",
            "duration": StripeCoupon.DURATION_FOREVER,
            "duration_in_months": None,
            "livemode": False,
            "max_redemptions": None,
            "metadata": {},
            "percent_off": 25,
            "redeem_by": created + 60,
            "times_redeemed": 0,
            "valid": True,
        }
        with requests_mock.Mocker() as m:
            m.register_uri(
                "POST",
                "https://api.stripe.com/v1/coupons",
                text=json.dumps(stripe_response),
            )
            coupon = StripeCoupon.objects.create(
                duration=StripeCoupon.DURATION_FOREVER,
                percent_off=25,
                redeem_by=timezone.now() + timedelta(seconds=60),
            )
            self.assertEqual(coupon.coupon_id, stripe_response["id"])
            self.assertEqual(
                coupon.created,
                timestamp_to_timezone_aware_date(stripe_response["created"]),
            )
            self.assertEqual(coupon.stripe_response, stripe_response)
            self.assertIn(
                "redeem_by={}".format(dateformat.format(coupon.redeem_by, "U")),
                m.last_request.body,
            )

            # test creating coupon with coupon_id
            stripe_response["id"] = "YOLO1"
            m.register_uri(
                "POST",
                "https://api.stripe.com/v1/coupons",
                text=json.dumps(stripe_response),
            )
            coupon = StripeCoupon.objects.create(
                coupon_id=stripe_response["id"], duration=StripeCoupon.DURATION_FOREVER
            )
            self.assertEqual(coupon.coupon_id, stripe_response["id"])

    def test_update(self):
        with requests_mock.Mocker() as m:
            created = int(time.mktime(datetime.now().timetuple()))
            stripe_response = {
                "id": "25OFF",
                "object": "coupon",
                "amount_off": 1,
                "created": created,
                "currency": "usd",
                "duration": StripeCoupon.DURATION_FOREVER,
                "duration_in_months": None,
                "livemode": False,
                "max_redemptions": None,
                "metadata": {},
                "percent_off": 25,
                "redeem_by": created + 60,
                "times_redeemed": 0,
                "valid": True,
            }
            coupon = self._create_coupon(coupon_id="25OFF", duration=StripeCoupon.DURATION_FOREVER, amount_off=1)
            self.assertFalse(coupon.is_deleted)

            # try accessing coupon that does not exist - should delete the coupon from our database
            m.register_uri(
                "GET",
                "https://api.stripe.com/v1/coupons/25OFF",
                status_code=404,
                text=json.dumps({"error": {"type": "invalid_request_error"}}),
            )
            coupon.metadata = {"yes": "no"}
            coupon.save()
            self.assertTrue(StripeCoupon.objects.deleted().filter(pk=coupon.pk).exists())

            # try changing other Stripe data than coupon's metadata
            m.register_uri(
                "GET",
                "https://api.stripe.com/v1/coupons/25OFF",
                text=json.dumps(stripe_response),
            )
            m.register_uri(
                "POST",
                "https://api.stripe.com/v1/coupons/25OFF",
                text=json.dumps(stripe_response),
            )
            coupon = self._create_coupon(coupon_id="25OFF", duration=StripeCoupon.DURATION_FOREVER, amount_off=1)
            coupon.duration = StripeCoupon.DURATION_ONCE
            coupon.save()
            coupon.refresh_from_db()
            self.assertNotEqual(coupon.duration, StripeCoupon.DURATION_ONCE)
            self.assertEqual(
                coupon.redeem_by,
                timestamp_to_timezone_aware_date(stripe_response["redeem_by"]),
            )

    def test_delete(self):
        coupon = self._create_coupon(coupon_id="CPON", amount_off=1, duration=StripeCoupon.DURATION_FOREVER)
        self.assertEqual(StripeCoupon.objects.deleted().count(), 0)
        stripe_response = {
            "id": "CPON",
            "object": "coupon",
            "amount_off": 100,
            "created": int(time.mktime(datetime.now().timetuple())),
            "currency": "usd",
            "duration": StripeCoupon.DURATION_FOREVER,
            "duration_in_months": None,
            "livemode": False,
            "max_redemptions": None,
            "metadata": {},
            "percent_off": 25,
            "redeem_by": None,
            "times_redeemed": 0,
            "valid": True,
        }
        with requests_mock.Mocker() as m:
            for coupon_name in ["CPON", "CPON2", "CPON3"]:
                for method in ["GET", "DELETE"]:
                    m.register_uri(
                        method,
                        "https://api.stripe.com/v1/coupons/{}".format(coupon_name),
                        text=json.dumps(stripe_response),
                    )
            coupon.delete()
            self.assertEqual(StripeCoupon.objects.deleted().count(), 1)

            # also test the overriden queryset's delete
            coupon2 = self._create_coupon(coupon_id="CPON2")
            coupon3 = self._create_coupon(coupon_id="CPON3")
            self.assertEqual(StripeCoupon.objects.filter(is_deleted=False).count(), 2)
            delete_result = StripeCoupon.objects.filter(pk__in=[coupon2.pk, coupon3.pk]).delete()
            self.assertEqual(delete_result, (2, {"aa_stripe.StripeCoupon": 2}))
            self.assertEqual(StripeCoupon.objects.deleted().count(), 3)
            self.assertEqual(StripeCoupon.objects.filter(is_deleted=False).count(), 0)

    def test_admin_form(self):
        # test correct creation
        data = {
            "coupon_id": "25OFF",
            "amount_off": 1,
            "currency": "usd",
            "duration": StripeCoupon.DURATION_ONCE,
            "metadata": {},
            "times_redeemed": 0,
            "valid": True,
        }
        self.assertTrue(StripeCouponForm(data=data).is_valid())

        # test passing none of amount_off or percent_off
        del data["amount_off"]
        self.assertFalse(StripeCouponForm(data=data).is_valid())

        # test passing both of amount_off and percent_off
        data["amount_off"] = 100
        data["percent_off"] = 10
        self.assertFalse(StripeCouponForm(data=data).is_valid())
        del data["percent_off"]

        # test passing amount_off without currency
        del data["currency"]
        self.assertFalse(StripeCouponForm(data=data).is_valid())
        data["currency"] = "usd"

        # test passing duration repeating with empty duration_in_months
        data["duration"] = StripeCoupon.DURATION_REPEATING
        self.assertFalse(StripeCouponForm(data=data).is_valid())

        # test passing duration_in_months when duration is not repeating
        data["duration"] = StripeCoupon.DURATION_ONCE
        data["duration_in_months"] = 1
        self.assertFalse(StripeCouponForm(data=data).is_valid())
        del data["duration_in_months"]

        stripe_response = {
            "id": "25OFF",
            "object": "coupon",
            "amount_off": 1,
            "created": int(time.mktime(datetime.now().timetuple())),
            "currency": "usd",
            "duration": StripeCoupon.DURATION_FOREVER,
            "duration_in_months": None,
            "livemode": False,
            "max_redemptions": None,
            "metadata": {},
            "percent_off": 25,
            "redeem_by": None,
            "times_redeemed": 0,
            "valid": True,
        }
        with requests_mock.Mocker() as m:
            for method in ["GET", "POST", "DELETE"]:
                m.register_uri(
                    method,
                    "https://api.stripe.com/v1/coupons/25OFF",
                    text=json.dumps(stripe_response),
                )
            coupon = self._create_coupon(data["coupon_id"], amount_off=1)

            # test creating a new coupon, when there is one that is not deleted
            self.assertTrue(StripeCoupon.objects.filter(coupon_id=data["coupon_id"], is_deleted=False).exists())
            self.assertFalse(StripeCouponForm(data=data).is_valid())

            # delete and try again
            coupon.is_deleted = True
            coupon.save()
            self.assertTrue(StripeCouponForm(data=data).is_valid())

    def test_details_api(self):
        # test accessing without authentication
        url = reverse("stripe-coupon-details", kwargs={"coupon_id": "FAKE-COUPON"})
        response = self.client.get(url, format="json")
        self.assertEqual(response.status_code, 403)

        user = UserModel.objects.create(email="foo@bar.bar", username="foo", password="dump-password")
        self.client.force_authenticate(user=user)
        # test accessing coupon that does not exist
        response = self.client.get(url, format="json")
        self.assertEqual(response.status_code, 404)

        # test regular
        coupon = self._create_coupon("COUPON")
        url = reverse("stripe-coupon-details", kwargs={"coupon_id": coupon.coupon_id})
        response = self.client.get(url, format="json")
        self.assertEqual(response.status_code, 200)
        self.assertEqual(
            set(response.data.keys()),
            {
                "coupon_id",
                "amount_off",
                "currency",
                "duration",
                "duration_in_months",
                "livemode",
                "max_redemptions",
                "metadata",
                "percent_off",
                "redeem_by",
                "times_redeemed",
                "valid",
                "is_created_at_stripe",
                "created",
                "updated",
                "is_deleted",
            },
        )

        # test accessing coupon that has already been deleted
        # update does not call object's .save(), so we do not need to mock Stripe API
        StripeCoupon.objects.filter(pk=coupon.pk).update(is_deleted=True)
        response = self.client.get(url, format="json")
        self.assertEqual(response.status_code, 404)

    def test_refresh_coupons_command(self):
        coupons = {
            "1A": self._create_coupon("1A"),
            "2A": self._create_coupon("2A"),
            "3A": self._create_coupon("3A"),
            "4A": self._create_coupon("4A", amount_off=100),
        }
        self.assertEqual(StripeCoupon.objects.count(), 4)
        # fake deleted coupon, this coupon should be recreated in the database
        StripeCoupon.objects.filter(pk=coupons["3A"].pk).update(is_deleted=True)

        coupon_1a_new_response = coupons["1A"].stripe_response.copy()
        coupon_1a_new_response["metadata"] = {"new": "data"}
        coupon_4a_new_response = coupons["4A"].stripe_response.copy()
        coupon_4a_new_response["created"] += 1
        coupon_4a_new_response["amount_off"] = 9999
        # fake limit
        stripe_response_part1 = {
            "object": "list",
            "url": "/v1/coupons",
            "has_more": True,
            "data": [
                coupon_1a_new_response,  # 1A will be updated, # 2A will be deleted
                coupons["3A"].stripe_response,  # 3A will be recreated
                coupon_4a_new_response,  # 4A will be deleted and recreated with the same name
            ],
        }
        new_coupon_stripe_response = coupons["1A"].stripe_response.copy()
        new_coupon_stripe_response["id"] = "1B"
        stripe_response_part2 = stripe_response_part1.copy()
        stripe_response_part2.update({"has_more": False, "data": [new_coupon_stripe_response]})
        with requests_mock.Mocker() as m:
            m.register_uri(
                "GET",
                "https://api.stripe.com/v1/coupons",
                text=json.dumps(stripe_response_part1),
            )
            m.register_uri(
                "GET",
                "https://api.stripe.com/v1/coupons?starting_after=4A",
                text=json.dumps(stripe_response_part2),
            )
            # 3A will be recreated
            m.register_uri(
                "GET",
                "https://api.stripe.com/v1/coupons/3A",
                text=json.dumps(coupons["3A"].stripe_response),
            )
            # 4A will be recreated with new data
            m.register_uri(
                "GET",
                "https://api.stripe.com/v1/coupons/4A",
                text=json.dumps(coupon_4a_new_response),
            )
            # 1B will be created
            m.register_uri(
                "GET",
                "https://api.stripe.com/v1/coupons/1B",
                text=json.dumps(new_coupon_stripe_response),
            )

            call_command("refresh_coupons")
            self.assertEqual(StripeCoupon.objects.all_with_deleted().count(), 7)  # 4 + 3 were created
            for coupon_id, coupon in coupons.items():
                coupons[coupon_id] = StripeCoupon.objects.all_with_deleted().get(pk=coupon.pk)

            self.assertEqual(coupons["1A"].metadata, coupon_1a_new_response["metadata"])
            self.assertTrue(coupons["2A"].is_deleted)
            new_3a_coupon = StripeCoupon.objects.get(coupon_id="3A", is_deleted=False)
            self.assertNotEqual(new_3a_coupon.pk, coupons["3A"].pk)
            self.assertTrue(coupons["4A"].is_deleted)
            new_4a_coupon = StripeCoupon.objects.get(coupon_id="4A", is_deleted=False)
            self.assertNotEqual(new_4a_coupon.pk, coupons["4A"].pk)
            self.assertEqual(
                new_4a_coupon.amount_off,
                Decimal(coupon_4a_new_response["amount_off"]) / 100,
            )
            self.assertTrue(StripeCoupon.objects.filter(coupon_id="1B", is_deleted=False).exists)
