import json
from unittest import IsolatedAsyncioTestCase
from unittest.mock import AsyncMock, MagicMock

from async_stripe import stripe
from django.test import RequestFactory, TestCase
from stripe.util import convert_to_stripe_object

from tordonate import container
from tordonate.civicrm.repository import CivicrmRepositoryMock
from tordonate.stripe import StripeController
from tordonate.stripe.controller import (StripeAddressData, StripeCustomerData, StripeMetadata,
                                         StripePaymentIntentData, StripeShippingData)


class StripeControllerTests(TestCase):
    def test_controller_init(self) -> None:
        ControllerTest = StripeController(
            "a", "b", "c", False, CivicrmRepositoryMock,
        )
        self.assertEqual(ControllerTest._StripeController__api_key, "a")  # type: ignore
        self.assertEqual(ControllerTest._StripeController__api_secret, "b")  # type: ignore
        self.assertEqual(ControllerTest._StripeController__webhook_secret, "c")  # type: ignore

    def test_stripe_exception_handler(self) -> None:
        ControllerTest = StripeController(
            "a", "b", "c", False, CivicrmRepositoryMock,
        )
        ControllerTest.stripe_exception_handler(stripe.error.CardError(
            "Card error",
            0,
            "card_declined"
        ))
        ControllerTest.stripe_exception_handler(stripe.error.RateLimitError())
        ControllerTest.stripe_exception_handler(stripe.error.InvalidRequestError("Card error", 0))
        ControllerTest.stripe_exception_handler(stripe.error.AuthenticationError())
        ControllerTest.stripe_exception_handler(stripe.error.APIConnectionError("Card error"))
        ControllerTest.stripe_exception_handler(stripe.error.StripeError())


class AsyncStripeControllerTests(IsolatedAsyncioTestCase):
    async def test_create_payment_intent(self) -> None:
        with open('./tordonate/tests/stripe/json/stripe_create_payment_intent.json') as f:
            mock_intent = convert_to_stripe_object(json.load(f))
        stripe.PaymentIntent = AsyncMock()
        stripe.PaymentIntent.create.return_value = mock_intent
        with stripe.PaymentIntent:
            ControllerTest = StripeController(
                "a", "b", "c", False, CivicrmRepositoryMock,
            )
            self.assertEqual(
                await ControllerTest.create_payment_intent(
                    StripePaymentIntentData(
                        amount=500,
                        receipt_email="user@example.com",
                        metadata=StripeMetadata(),
                        shipping=StripeShippingData(
                            name="John Doe",
                            address=StripeAddressData(
                                city="New York",
                                country="US",
                                line1="123 Sesame Street",
                                line2="2nd floor",
                                postal_code="123456",
                                state="NY"
                            )
                        )
                    )
                ),
                mock_intent)

    async def test_create_payment_intent_api_down(self) -> None:
        stripe.PaymentIntent = AsyncMock()
        stripe.PaymentIntent.create.side_effect = stripe.error.APIConnectionError("Cannot connect")
        with stripe.PaymentIntent:
            ControllerTest = StripeController(
                "a", "b", "c", False, CivicrmRepositoryMock,
            )
            with self.assertRaises(ValueError):
                await ControllerTest.create_payment_intent(
                    StripePaymentIntentData(
                        amount=500,
                        receipt_email="user@example.com",
                        metadata=StripeMetadata(),
                        shipping=StripeShippingData(
                            name="John Doe",
                            address=StripeAddressData(
                                city="New York",
                                country="US",
                                line1="123 Sesame Street",
                                line2="2nd floor",
                                postal_code="123456",
                                state="NY"
                            )
                        )
                    )
                )

    async def test_get_or_create_monthly_plan_exists(self) -> None:
        with open('./tordonate/tests/stripe/json/stripe_create_monthly_plan.json') as f:
            mock_intent = convert_to_stripe_object(json.load(f))
        stripe.Plan = AsyncMock()
        stripe.Plan.retrieve.return_value = mock_intent
        with stripe.PaymentIntent:
            ControllerTest = StripeController(
                "a", "b", "c", False, CivicrmRepositoryMock,
            )
            self.assertEqual(await ControllerTest.get_or_create_monthly_plan(500), mock_intent)

    async def test_get_or_create_monthly_plan_doesnt_exist(self) -> None:
        with open('./tordonate/tests/stripe/json/stripe_create_monthly_plan.json') as f:
            mock_intent = convert_to_stripe_object(json.load(f))
        stripe.Plan = AsyncMock()
        stripe.Plan.retrieve.side_effect = stripe.error.InvalidRequestError("Not found", 0)
        stripe.Plan.create.return_value = mock_intent
        with stripe.PaymentIntent:
            ControllerTest = StripeController(
                "a", "b", "c", False, CivicrmRepositoryMock,
            )
            self.assertEqual(await ControllerTest.get_or_create_monthly_plan(500), mock_intent)

    #
    async def test_get_or_create_monthly_plan_api_down(self) -> None:
        stripe.Plan = AsyncMock()
        stripe.Plan.retrieve.side_effect = stripe.error.APIConnectionError("Cannot connect")
        with stripe.PaymentIntent:
            ControllerTest = StripeController(
                "a", "b", "c", False, CivicrmRepositoryMock,
            )
            with self.assertRaises(ValueError):
                await ControllerTest.get_or_create_monthly_plan(500)

    async def test_get_or_create_monthly_plan_api_down_suddenly(self) -> None:
        stripe.Plan = AsyncMock()
        stripe.Plan.retrieve.side_effect = stripe.error.InvalidRequestError("Not found", 0)
        stripe.Plan.create.side_effect = stripe.error.APIConnectionError("Cannot connect")
        with stripe.PaymentIntent:
            ControllerTest = StripeController(
                "a", "b", "c", False, CivicrmRepositoryMock,
            )
            with self.assertRaises(ValueError):
                await ControllerTest.get_or_create_monthly_plan(500)

    async def test_create_customer(self) -> None:
        with open('./tordonate/tests/stripe/json/stripe_create_customer.json') as f:
            mock_intent = convert_to_stripe_object(json.load(f))
        stripe.Customer = AsyncMock()
        stripe.Customer.create.return_value = mock_intent
        with stripe.Customer:
            ControllerTest = StripeController(
                "a", "b", "c", False, CivicrmRepositoryMock,
            )
            self.assertEqual(
                await ControllerTest.create_customer(
                    StripeCustomerData(
                        name="John Doe",
                        email="user@example.com",
                        address=StripeAddressData(
                            city="New York",
                            country="US",
                            line1="123 Sesame Street",
                            line2="2nd floor",
                            postal_code="123456",
                            state="NY"
                        )
                    )
                ),
                mock_intent
            )

    async def test_create_customer_api_error(self) -> None:
        stripe.Customer = AsyncMock()
        stripe.Customer.create.side_effect = stripe.error.StripeError
        with stripe.Customer:
            ControllerTest = StripeController(
                "a", "b", "c", False, CivicrmRepositoryMock,
            )
            with self.assertRaises(ValueError):
                await ControllerTest.create_customer(
                    StripeCustomerData(
                        name="John Doe",
                        email="user@example.com",
                        address=StripeAddressData(
                            city="New York",
                            country="US",
                            line1="123 Sesame Street",
                            line2="2nd floor",
                            postal_code="123456",
                            state="NY"
                        )
                    )
                )

    async def test_create_subscription(self) -> None:
        with open('./tordonate/tests/stripe/json/stripe_create_customer.json') as f:
            mock_intent = convert_to_stripe_object(json.load(f))
        stripe.Subscription = AsyncMock()
        stripe.Subscription.create.return_value = mock_intent
        with stripe.Subscription:
            ControllerTest = StripeController(
                "a", "b", "c", False, CivicrmRepositoryMock,
            )
            self.assertEqual(
                await ControllerTest.create_subscription(
                    "cus_Q5b2bZbfE8vhAs",
                    "pi_3PFPs1APRiS9lYuI3pkK4CQ6",
                    StripeMetadata()
                ), mock_intent
            )

    async def test_create_subscription_api_error(self) -> None:
        stripe.Subscription = AsyncMock()
        stripe.Subscription.create.side_effect = stripe.error.StripeError
        with stripe.Customer:
            ControllerTest = StripeController(
                "a", "b", "c", False, CivicrmRepositoryMock,
            )
            with self.assertRaises(ValueError):
                await ControllerTest.create_subscription(
                    "cus_Q5b2bZbfE8vhAs",
                    "pi_3PFPs1APRiS9lYuI3pkK4CQ6",
                    StripeMetadata()
                )

    async def test_validate_webhook(self) -> None:
        with open('./tordonate/tests/stripe/json/stripe_webhook_validation.json') as f:
            mock_response = json.load(f)
        with open('./tordonate/tests/stripe/json/stripe_webhook_request_headers.json') as f:
            mock_headers = json.load(f)
        with open('./tordonate/tests/stripe/json/stripe_webhook_request_body.json') as f:
            mock_body = json.load(f)
        stripe.Webhook = MagicMock()
        stripe.Webhook.construct_event.return_value = mock_response
        with stripe.Webhook:
            ControllerTest = StripeController(
                "a", "b", "c", False, CivicrmRepositoryMock,
            )
            self.factory = RequestFactory()
            request = self.factory.post(
                "/stripe/webhook/",
                mock_body,
                content_type='application/json',
                headers=mock_headers,
            )
            self.assertEqual(
                await ControllerTest.validate_webhook(request),  # type: ignore
                None
            )

    async def test_validate_webhook_invalid(self) -> None:
        with open('./tordonate/tests/stripe/json/stripe_webhook_request_headers.json') as f:
            mock_headers = json.load(f)
        with open('./tordonate/tests/stripe/json/stripe_webhook_request_body.json') as f:
            mock_body = json.load(f)
        stripe.Webhook = MagicMock()
        stripe.Webhook.construct_event.side_effect = stripe.error.SignatureVerificationError(
            "Error verifying webhook signature",
            0
        )
        with stripe.Webhook:
            ControllerTest = StripeController(
                "a", "b", "c", False, CivicrmRepositoryMock,
            )
            self.factory = RequestFactory()
            request = self.factory.post(
                "/stripe/webhook/",
                mock_body,
                content_type='application/json',
                headers=mock_headers,
            )
            with self.assertRaises(ValueError):
                await ControllerTest.validate_webhook(request)

    async def test_validate_webhook_api_down(self) -> None:
        with open('./tordonate/tests/stripe/json/stripe_webhook_request_headers.json') as f:
            mock_headers = json.load(f)
        with open('./tordonate/tests/stripe/json/stripe_webhook_request_body.json') as f:
            mock_body = json.load(f)
        stripe.Webhook = MagicMock()
        stripe.Webhook.construct_event.side_effect = ValueError
        with stripe.Webhook:
            ControllerTest = StripeController(
                "a", "b", "c", False, CivicrmRepositoryMock,
            )
            self.factory = RequestFactory()
            request = self.factory.post(
                "/stripe/webhook/",
                mock_body,
                content_type='application/json',
                headers=mock_headers,
            )
            with self.assertRaises(ValueError):
                await ControllerTest.validate_webhook(request)

    async def test_process_webhook(self) -> None:
        with open('./tordonate/tests/stripe/json/stripe_webhook_request_headers.json') as f:
            mock_headers = json.load(f)
        with open('./tordonate/tests/stripe/json/stripe_webhook_request_body.json') as f:
            mock_body = json.load(f)
        civi_mock = MagicMock(spec=CivicrmRepositoryMock)
        with container.civi.override(civi_mock):
            ControllerTest = StripeController(
                "a", "b", "c", False, civi_mock,
            )
            self.factory = RequestFactory()
            request = self.factory.post(
                "/stripe/webhook/",
                mock_body,
                content_type='application/json',
                headers=mock_headers,
            )
            self.assertEqual(
                await ControllerTest.process_webhook(request),  # type: ignore
                None
            )

    async def test_process_webhook_payment_failed(self) -> None:
        with open('./tordonate/tests/stripe/json/stripe_webhook_request_headers.json') as f:
            mock_headers = json.load(f)
        with open('./tordonate/tests/stripe/json/stripe_webhook_request_failed_body.json') as f:
            mock_body = json.load(f)
        civi_mock = MagicMock(spec=CivicrmRepositoryMock)
        with container.civi.override(civi_mock):
            ControllerTest = StripeController(
                "a", "b", "c", False, civi_mock,
            )
            self.factory = RequestFactory()
            request = self.factory.post(
                "/stripe/webhook/",
                mock_body,
                content_type='application/json',
                headers=mock_headers,
            )
            self.assertEqual(
                await ControllerTest.process_webhook(request),  # type: ignore
                None
            )

    async def test_process_webhook_civi_down(self) -> None:
        with open('./tordonate/tests/stripe/json/stripe_webhook_request_headers.json') as f:
            mock_headers = json.load(f)
        with open('./tordonate/tests/stripe/json/stripe_webhook_request_body.json') as f:
            mock_body = json.load(f)
        civi_mock = MagicMock(spec=CivicrmRepositoryMock)
        civi_mock.donation_exists.return_value = False
        civi_mock.report_donation.side_effect = ValueError
        with container.civi.override(civi_mock):
            ControllerTest = StripeController(
                "a", "b", "c", False, civi_mock,
            )
            self.factory = RequestFactory()
            request = self.factory.post(
                "/stripe/webhook/",
                mock_body,
                content_type='application/json',
                headers=mock_headers,
            )
            with self.assertRaises(ValueError):
                await ControllerTest.process_webhook(request)
