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

import requests_mock
from django.test import TestCase

from tordonate import container
from tordonate.civicrm.repository import CivicrmRepositoryMock
from tordonate.paypal import PaypalController


class PaypalControllerTests(TestCase):
    def test_controller_init(self) -> None:
        ControllerTest = PaypalController(
            "a", "b", "c", "d", False, CivicrmRepositoryMock
        )
        self.assertEqual(ControllerTest._PaypalController__client_id, "a")  # type: ignore
        self.assertEqual(ControllerTest._PaypalController__app_secret, "b")  # type: ignore
        self.assertEqual(ControllerTest._PaypalController__sub_product_id, "c")  # type: ignore
        self.assertEqual(ControllerTest._PaypalController__webhook_id, "d")  # type: ignore

    def test_is_payment_paypal_formatted(self) -> None:
        self.assertTrue(PaypalController.isPaymentPaypalFormatted(self, "10.00"))
        self.assertFalse(PaypalController.isPaymentPaypalFormatted(self, "1000"))
        self.assertFalse(PaypalController.isPaymentPaypalFormatted(self, "one thousand"))
        self.assertFalse(PaypalController.isPaymentPaypalFormatted(self, 1000))  # type: ignore


class AsyncPaypalControllerTests(IsolatedAsyncioTestCase):
    with open("./tordonate/tests/paypal/data/paypal_ipn_successful.txt", "rb") as f:
        sandbox_ipn = f.read()
    with open("./tordonate/tests/paypal/data/paypal_ipn_failed.txt", "rb") as f:
        sandbox_ipn_failed = f.read()
    with open("./tordonate/tests/paypal/data/paypal_ipn_wrong_type.txt", "rb") as f:
        sandbox_ipn_wrong_type = f.read()
    with open("./tordonate/tests/paypal/data/paypal_ipn_too_new.txt", "rb") as f:
        sandbox_ipn_too_new = f.read()

    async def test_generate_access_token(self) -> None:
        # The access token found in this JSON file was generated with sandbox credentials and
        # has expired; it cannot be reused to forge new REST API calls.
        with open('./tordonate/tests/paypal/data/paypal_generate_access_token.json') as f:
            mock_token = json.load(f)
        with requests_mock.Mocker() as m:
            ControllerTest = PaypalController(
                "a", "b", "c", "d", False, CivicrmRepositoryMock
            )
            m.register_uri(
                'POST',
                f'{ControllerTest.base_url}/v1/oauth2/token',
                json=mock_token
            )
            self.assertEqual(
                await ControllerTest.generate_access_token(),
                'A21AAJbI_bIkGDL8Cw83Rtu4Xhb-6EdiAQMb1UwK2ECsFV-'
                'Z6KH2gsNFTtFOev5_mUjCWIqlzu8yqC9zvY5mKIzg5D5ehkBXQ'
            )

    async def test_generate_access_token_api_down(self) -> None:
        with requests_mock.Mocker() as m:
            ControllerTest = PaypalController(
                "a", "b", "c", "d", False, CivicrmRepositoryMock
            )
            m.register_uri(
                "POST",
                f'{ControllerTest.base_url}/v1/oauth2/token',
                status_code=503
            )
            with self.assertRaises(ValueError):
                await ControllerTest.generate_access_token()

    async def test_create_order(self) -> None:
        with open('./tordonate/tests/paypal/data/paypal_generate_access_token.json') as f:
            mock_token = json.load(f)
        with open('./tordonate/tests/paypal/data/paypal_create_order.json') as f:
            mock_order_return = json.load(f)
        with requests_mock.Mocker() as m:
            ControllerTest = PaypalController(
                "a", "b", "c", "d", False, CivicrmRepositoryMock
            )
            m.register_uri(
                'POST',
                f'{ControllerTest.base_url}/v1/oauth2/token',
                json=mock_token
            )
            m.register_uri(
                "POST",
                f'{ControllerTest.base_url}/v2/checkout/orders',
                json=mock_order_return
            )
            self.assertEqual(
                await ControllerTest.create_order("5.00"),
                {"id": "3JG19427UA619722H"}
            )

    async def test_create_order_bad_input(self) -> None:
        ControllerTest = PaypalController(
            "a", "b", "c", "d", False, CivicrmRepositoryMock
        )
        self.assertFalse(ControllerTest.isPaymentPaypalFormatted(1000))  # type: ignore
        with self.assertRaises(ValueError):
            await ControllerTest.create_order("1000")

    async def test_create_order_api_down(self) -> None:
        with open('./tordonate/tests/paypal/data/paypal_generate_access_token.json') as f:
            mock_token = json.load(f)
        with requests_mock.Mocker() as m:
            ControllerTest = PaypalController(
                "a", "b", "c", "d", False, CivicrmRepositoryMock
            )
            m.register_uri(
                'POST',
                f'{ControllerTest.base_url}/v1/oauth2/token',
                json=mock_token
            )
            m.register_uri(
                "POST",
                f'{ControllerTest.base_url}/v2/checkout/orders',
                status_code=503
            )
            with self.assertRaises(ValueError):
                await ControllerTest.create_order("10.00")

    async def test_capture_payment(self) -> None:
        with open('./tordonate/tests/paypal/data/paypal_generate_access_token.json') as f:
            mock_token = json.load(f)
        with open('./tordonate/tests/paypal/data/paypal_capture_payment.json') as f:
            mock_order_return = json.load(f)
        with requests_mock.Mocker() as m:
            ControllerTest = PaypalController(
                "a", "b", "c", "d", False, CivicrmRepositoryMock
            )
            m.register_uri(
                'POST',
                f'{ControllerTest.base_url}/v1/oauth2/token',
                json=mock_token
            )
            m.register_uri(
                "POST",
                f'{ControllerTest.base_url}/v2/checkout/orders/order_id/capture',
                json=mock_order_return
            )
            self.assertEqual(
                await ControllerTest.capture_payment("order_id"),
                mock_order_return
            )

    async def test_capture_payment_bad_input(self) -> None:
        with open('./tordonate/tests/paypal/data/paypal_generate_access_token.json') as f:
            mock_token = json.load(f)
        with requests_mock.Mocker() as m:
            ControllerTest = PaypalController(
                "a", "b", "c", "d", False, CivicrmRepositoryMock
            )
            m.register_uri(
                'POST',
                f'{ControllerTest.base_url}/v1/oauth2/token',
                json=mock_token
            )
            m.register_uri(
                "POST",
                f'{ControllerTest.base_url}/v2/checkout/orders/bad_order_id/capture',
                status_code=401
            )
            with self.assertRaises(ValueError):
                await ControllerTest.capture_payment("bad_order_id")

    async def test_capture_payment_api_down(self) -> None:
        with open('./tordonate/tests/paypal/data/paypal_generate_access_token.json') as f:
            mock_token = json.load(f)
        with requests_mock.Mocker() as m:
            ControllerTest = PaypalController(
                "a", "b", "c", "d", False, CivicrmRepositoryMock
            )
            m.register_uri(
                'POST',
                f'{ControllerTest.base_url}/v1/oauth2/token',
                json=mock_token
            )
            m.register_uri(
                "POST",
                f'{ControllerTest.base_url}/v2/checkout/orders/order_id/capture',
                status_code=503
            )
            with self.assertRaises(ValueError):
                await ControllerTest.capture_payment("order_id")

    async def test_verify_product(self) -> None:
        with open('./tordonate/tests/paypal/data/paypal_generate_access_token.json') as f:
            mock_token = json.load(f)
        with open('./tordonate/tests/paypal/data/paypal_verify_product.json') as f:
            mock_verification = json.load(f)
        with requests_mock.Mocker() as m:
            product_id = "b9e71bcd19e545c09e2489c105981ed2"
            ControllerTest = PaypalController(
                "a", "b", "c", "d", False, CivicrmRepositoryMock
            )
            m.register_uri(
                'POST',
                f'{ControllerTest.base_url}/v1/oauth2/token',
                json=mock_token
            )
            m.register_uri(
                'GET',
                f'{ControllerTest.base_url}/v1/catalogs/products/{product_id}',
                json=mock_verification
            )
            self.assertTrue(
                await ControllerTest.verify_product("b9e71bcd19e545c09e2489c105981ed2")
            )

    async def test_verify_product_not_found(self) -> None:
        with open('./tordonate/tests/paypal/data/paypal_generate_access_token.json') as f:
            mock_token = json.load(f)
        with requests_mock.Mocker() as m:
            product_id = "b9e71bcd19e545c09e2489c105981ed2"
            ControllerTest = PaypalController(
                "a", "b", "c", "d", False, CivicrmRepositoryMock
            )
            m.register_uri(
                'POST',
                f'{ControllerTest.base_url}/v1/oauth2/token',
                json=mock_token
            )
            m.register_uri(
                'GET',
                f'{ControllerTest.base_url}/v1/catalogs/products/{product_id}',
                status_code=404
            )
            self.assertFalse(
                await ControllerTest.verify_product("b9e71bcd19e545c09e2489c105981ed2")
            )

    async def test_verify_product_api_down(self) -> None:
        with open('./tordonate/tests/paypal/data/paypal_generate_access_token.json') as f:
            mock_token = json.load(f)
        with requests_mock.Mocker() as m:
            product_id = "b9e71bcd19e545c09e2489c105981ed2"
            ControllerTest = PaypalController(
                "a", "b", "c", "d", False, CivicrmRepositoryMock
            )
            m.register_uri(
                'POST',
                f'{ControllerTest.base_url}/v1/oauth2/token',
                json=mock_token
            )
            m.register_uri(
                'GET',
                f'{ControllerTest.base_url}/v1/catalogs/products/{product_id}',
                status_code=503
            )
            with self.assertRaises(ValueError):
                await ControllerTest.verify_product("b9e71bcd19e545c09e2489c105981ed2")

    async def test_get_or_create_product(self) -> None:
        with open('./tordonate/tests/paypal/data/paypal_generate_access_token.json') as f:
            mock_token = json.load(f)
        with open('./tordonate/tests/paypal/data/paypal_verify_product.json') as f:
            mock_verification = json.load(f)
        with requests_mock.Mocker() as m:
            ControllerTest = PaypalController(
                "a", "b", "c", "d", False, CivicrmRepositoryMock
            )
            m.register_uri(
                'POST',
                f'{ControllerTest.base_url}/v1/oauth2/token',
                json=mock_token
            )
            m.register_uri(
                'GET',
                f'{ControllerTest.base_url}/v1/catalogs/products/c',
                json=mock_verification
            )
            self.assertEqual(
                await ControllerTest.get_or_create_product(),
                {"id": "c"}
            )

    async def test_get_or_create_product_not_found(self) -> None:
        with open('./tordonate/tests/paypal/data/paypal_generate_access_token.json') as f:
            mock_token = json.load(f)
        with open('./tordonate/tests/paypal/data/paypal_create_product.json') as f:
            mock_product = json.load(f)
        with requests_mock.Mocker() as m:
            ControllerTest = PaypalController(
                "a", "b", "c", "d", False, CivicrmRepositoryMock
            )
            m.register_uri(
                'POST',
                f'{ControllerTest.base_url}/v1/oauth2/token',
                json=mock_token
            )
            m.register_uri(
                'GET',
                f'{ControllerTest.base_url}/v1/catalogs/products/c',
                status_code=404
            )
            m.register_uri(
                'POST',
                f'{ControllerTest.base_url}/v1/catalogs/products',
                json=mock_product
            )
            self.assertEqual(
                await ControllerTest.get_or_create_product(),
                {"id": "c"}
            )

    # Tests API being fully down
    async def test_get_or_create_product_api_down(self) -> None:
        with open('./tordonate/tests/paypal/data/paypal_generate_access_token.json') as f:
            mock_token = json.load(f)
        with requests_mock.Mocker() as m:
            ControllerTest = PaypalController(
                "a", "b", "c", "d", False, CivicrmRepositoryMock
            )
            m.register_uri(
                'POST',
                f'{ControllerTest.base_url}/v1/oauth2/token',
                json=mock_token
            )
            m.register_uri(
                'GET',
                f'{ControllerTest.base_url}/v1/catalogs/products/c',
                status_code=503
            )
            m.register_uri(
                'POST',
                f'{ControllerTest.base_url}/v1/catalogs/products',
                status_code=503
            )
            with self.assertRaises(ValueError):
                await ControllerTest.get_or_create_product()

    # Tests API going down between verify_product() and get_or_create_product()
    async def test_get_or_create_product_api_suddenly_down(self) -> None:
        with open('./tordonate/tests/paypal/data/paypal_generate_access_token.json') as f:
            mock_token = json.load(f)
        with requests_mock.Mocker() as m:
            ControllerTest = PaypalController(
                "a", "b", "c", "d", False, CivicrmRepositoryMock
            )
            m.register_uri(
                'POST',
                f'{ControllerTest.base_url}/v1/oauth2/token',
                json=mock_token
            )
            m.register_uri(
                'GET',
                f'{ControllerTest.base_url}/v1/catalogs/products/c',
                status_code=404
            )
            m.register_uri(
                'POST',
                f'{ControllerTest.base_url}/v1/catalogs/products',
                status_code=503
            )
            with self.assertRaises(ValueError):
                await ControllerTest.get_or_create_product()

    async def test_create_subscription(self) -> None:
        with open('./tordonate/tests/paypal/data/paypal_generate_access_token.json') as f:
            mock_token = json.load(f)
        with open('./tordonate/tests/paypal/data/paypal_create_subscription.json') as f:
            mock_create_subscription = json.load(f)
        with requests_mock.Mocker() as m:
            ControllerTest = PaypalController(
                "a", "b", "c", "d", False, CivicrmRepositoryMock
            )
            m.register_uri(
                'POST',
                f'{ControllerTest.base_url}/v1/oauth2/token',
                json=mock_token
            )
            m.register_uri(
                'POST',
                f'{ControllerTest.base_url}/v1/billing/plans',
                json=mock_create_subscription
            )
            self.assertEqual(
                await ControllerTest.create_subscription(
                    "5.00",
                    "b9e71bcd19e545c09e2489c105981ed2"
                ),
                {"id": "P-8A295785056698723MY4F2UI"}
            )

    async def test_create_subscription_bad_input(self) -> None:
        with open('./tordonate/tests/paypal/data/paypal_generate_access_token.json') as f:
            mock_token = json.load(f)
        with open('./tordonate/tests/paypal/data/paypal_create_subscription.json') as f:
            mock_create_subscription = json.load(f)
        with requests_mock.Mocker() as m:
            ControllerTest = PaypalController(
                "a", "b", "c", "d", False, CivicrmRepositoryMock
            )
            m.register_uri(
                'POST',
                f'{ControllerTest.base_url}/v1/oauth2/token',
                json=mock_token
            )
            m.register_uri(
                'POST',
                f'{ControllerTest.base_url}/v1/billing/plans',
                json=mock_create_subscription
            )
            with self.assertRaises(ValueError):
                await ControllerTest.create_subscription(
                    "500",
                    "b9e71bcd19e545c09e2489c105981ed2"
                )

    async def test_create_subscription_api_down(self) -> None:
        with open('./tordonate/tests/paypal/data/paypal_generate_access_token.json') as f:
            mock_token = json.load(f)
        with requests_mock.Mocker() as m:
            ControllerTest = PaypalController(
                "a", "b", "c", "d", False, CivicrmRepositoryMock
            )
            m.register_uri(
                'POST',
                f'{ControllerTest.base_url}/v1/oauth2/token',
                json=mock_token
            )
            m.register_uri(
                'POST',
                f'{ControllerTest.base_url}/v1/billing/plans',
                status_code=503
            )
            with self.assertRaises(ValueError):
                await ControllerTest.create_subscription(
                    "5.00",
                    "b9e71bcd19e545c09e2489c105981ed2"
                )

    async def test_validate_webhook(self) -> None:
        with open('./tordonate/tests/paypal/data/paypal_generate_access_token.json') as f:
            mock_token = json.load(f)
        with open('./tordonate/tests/paypal/data/paypal_webhook_headers.json') as f:
            mock_headers = json.load(f)
        with open('./tordonate/tests/paypal/data/paypal_webhook_body-completed.json') as f:
            mock_body = json.load(f)
        with requests_mock.Mocker() as m:
            ControllerTest = PaypalController(
                "a", "b", "c", "d", False, CivicrmRepositoryMock
            )
            m.register_uri(
                'POST',
                f'{ControllerTest.base_url}/v1/oauth2/token',
                json=mock_token
            )
            m.register_uri(
                'POST',
                f'{ControllerTest.base_url}/v1/notifications/verify-webhook-signature',
                json={"verification_status": "SUCCESS"}
            )
            self.assertEqual(
                await ControllerTest.validate_webhook(mock_headers, mock_body),
                {"verification_status": "SUCCESS"}
            )

    async def test_validate_webhook_api_down(self) -> None:
        with open('./tordonate/tests/paypal/data/paypal_generate_access_token.json') as f:
            mock_token = json.load(f)
        with open('./tordonate/tests/paypal/data/paypal_webhook_headers.json') as f:
            mock_headers = json.load(f)
        with open('./tordonate/tests/paypal/data/paypal_webhook_body-completed.json') as f:
            mock_body = json.load(f)
        with requests_mock.Mocker() as m:
            ControllerTest = PaypalController(
                "a", "b", "c", "d", False, CivicrmRepositoryMock
            )
            m.register_uri(
                'POST',
                f'{ControllerTest.base_url}/v1/oauth2/token',
                json=mock_token
            )
            m.register_uri(
                'POST',
                f'{ControllerTest.base_url}/v1/notifications/verify-webhook-signature',
                status_code=503
            )
            with self.assertRaises(ValueError):
                await ControllerTest.validate_webhook(mock_headers, mock_body)

    async def test_process_webhook(self) -> None:
        with open('./tordonate/tests/paypal/data/paypal_webhook_body-completed.json') as f:
            mock_body = json.load(f)
        civi_mock = MagicMock(spec=CivicrmRepositoryMock)
        civi_mock.donation_exists.return_value = False
        civi_mock.report_donation.return_value = None
        with container.civi.override(civi_mock):
            ControllerTest = PaypalController(
                "a", "b", "c", "d", False, civi_mock
            )
            self.assertEqual(
                await ControllerTest.process_webhook(mock_body),  # type: ignore
                None
            )

    async def test_process_webhook_payment_failed(self) -> None:
        with open('./tordonate/tests/paypal/data/paypal_webhook_body-failed.json') as f:
            mock_body = json.load(f)
        civi_mock = MagicMock(spec=CivicrmRepositoryMock)
        civi_mock.donation_exists.return_value = False
        civi_mock.report_donation.return_value = None
        with container.civi.override(civi_mock):
            ControllerTest = PaypalController(
                "a", "b", "c", "d", False, civi_mock
            )
            self.assertEqual(
                await ControllerTest.process_webhook(mock_body),  # type: ignore
                None
            )

    async def test_process_webhook_civi_down(self) -> None:
        with open('./tordonate/tests/paypal/data/paypal_webhook_body-completed.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 = PaypalController(
                "a", "b", "c", "d", False, civi_mock
            )
            with self.assertRaises(ValueError):
                await ControllerTest.process_webhook(mock_body)

    async def test_validate_ipn(self) -> None:
        with requests_mock.Mocker() as m:
            ControllerTest = PaypalController(
                "a", "b", "c", "d", False, CivicrmRepositoryMock
            )
            m.register_uri(
                'POST',
                ControllerTest.ipn_url,
                text="VERIFIED"
            )
            self.assertEqual(
                await ControllerTest.validate_ipn(self.sandbox_ipn),
                True
            )

    async def test_validate_ipn_invalid(self) -> None:
        with requests_mock.Mocker() as m:
            ControllerTest = PaypalController(
                "a", "b", "c", "d", False, CivicrmRepositoryMock
            )
            m.register_uri(
                "POST",
                ControllerTest.ipn_url,
                text="INVALID"
            )
            self.assertEqual(
                await ControllerTest.validate_ipn(b"invalid_ipn"),
                False
            )

    async def test_validate_ipn_api_down(self) -> None:
        with requests_mock.Mocker() as m:
            ControllerTest = PaypalController(
                "a", "b", "c", "d", False, CivicrmRepositoryMock
            )
            m.register_uri(
                'POST',
                ControllerTest.ipn_url,
                status_code=502
            )
            with self.assertRaises(ValueError):
                await ControllerTest.validate_ipn(self.sandbox_ipn)

    async def test_process_ipn(self) -> None:
        civi_mock = MagicMock(spec=CivicrmRepositoryMock)
        civi_mock.donation_exists.return_value = False
        civi_mock.report_donation.return_value = None
        with container.civi.override(civi_mock):
            ControllerTest = PaypalController(
                "a", "b", "c", "d", False, civi_mock
            )
            self.assertEqual(
                await ControllerTest.process_ipn(self.sandbox_ipn),  # type: ignore
                None
            )
            civi_mock.donation_exists.assert_called()
            civi_mock.report_donation.assert_called()
            civi_mock.report_donation.assert_called_with(
                "Tor\\Donation\\RecurringContributionOngoing",
                {
                    "payment_instrument_id": "PayPal",
                    "receive_date": "2024-10-30 14:57:11",
                    "trxn_id": "I-JXHVH701URKS",
                    "handshake_id": "I-JXHVH701URKS",
                    "recurring_contribution_transaction_id": "I-JXHVH701URKS",
                    "currency": "USD",
                    "total_amount": "5.00",
                    "contribution_status_id": "Completed"
                }
            )

    async def test_process_ipn_wrong_type(self) -> None:
        civi_mock = MagicMock(spec=CivicrmRepositoryMock)
        civi_mock.donation_exists.return_value = False
        civi_mock.report_donation.return_value = None
        with container.civi.override(civi_mock):
            ControllerTest = PaypalController(
                "a", "b", "c", "d", False, civi_mock
            )
            self.assertEqual(
                await ControllerTest.process_ipn(self.sandbox_ipn_wrong_type),  # type: ignore
                None
            )
            civi_mock.donation_exists.assert_not_called()
            civi_mock.report_donation.assert_not_called()

    async def test_process_ipn_too_new(self) -> None:
        civi_mock = MagicMock(spec=CivicrmRepositoryMock)
        civi_mock.donation_exists.return_value = False
        civi_mock.report_donation.return_value = None
        with container.civi.override(civi_mock):
            ControllerTest = PaypalController(
                "a", "b", "c", "d", False, civi_mock
            )
            self.assertEqual(
                await ControllerTest.process_ipn(self.sandbox_ipn_too_new),  # type: ignore
                None
            )
            civi_mock.donation_exists.assert_not_called()
            civi_mock.report_donation.assert_not_called()

    async def test_process_ipn_payment_failed(self) -> None:
        civi_mock = MagicMock(spec=CivicrmRepositoryMock)
        civi_mock.donation_exists.return_value = False
        civi_mock.report_donation.return_value = None
        with container.civi.override(civi_mock):
            ControllerTest = PaypalController(
                "a", "b", "c", "d", False, civi_mock
            )
            self.assertEqual(
                await ControllerTest.process_ipn(self.sandbox_ipn_failed),  # type: ignore
                None
            )
            civi_mock.donation_exists.assert_called()
            civi_mock.report_donation.assert_called()
            civi_mock.report_donation.assert_called_with(
                "Tor\\Donation\\RecurringContributionOngoing",
                {
                    "payment_instrument_id": "PayPal",
                    "receive_date": "2024-10-30 14:57:11",
                    "trxn_id": "I-JXHVH701URKS",
                    "handshake_id": "I-JXHVH701URKS",
                    "recurring_contribution_transaction_id": "I-JXHVH701URKS",
                    "currency": "USD",
                    "total_amount": "5.00",
                    "contribution_status_id": "Failed"
                }
            )

    async def test_process_ipn_civi_down(self) -> None:
        civi_mock = MagicMock(spec=CivicrmRepositoryMock)
        civi_mock.donation_exists.return_value = False
        civi_mock.report_donation = AsyncMock(side_effect=ValueError)
        with container.civi.override(civi_mock):
            ControllerTest = PaypalController(
                "a", "b", "c", "d", False, civi_mock
            )
            with self.assertRaises(ValueError):
                await ControllerTest.process_ipn(self.sandbox_ipn)
