from unittest import mock

from django.test import TestCase

from tordonate.containers import Container
from tordonate.forms import DonorInfoFormB
from tordonate.utils import make_valid_payload


# When testing the donation form, be mindful of the fact that it expresses donation values in
# whole dollars, whereas the rest of the infrastructure keeps dollar amounts in cents.
# It is good practice to refrain from using hardcoded values in these tests, so that changing
# donation and perk minimums are automatically reflected in these test cases, so
# be mindful of how DonorInfoFormTests.setUp pulls values directly from civicrm.repository
# (which itself gets them fresh from source) and converts them for proper use in testing here.
class DonorInfoFormTests(TestCase):
    data = {
        "custom_donation": 25,
        "single_or_monthly": "single",
        "perk": "no_gift",
        "first_name": "first",
        "last_name": "last",
        "street_address": "street address",
        "apartment_number": "apartment",
        "country": "country",
        "city": "city",
        "state": "state",
        "postal_code": "90210",
        "email_address": "address@email.com",
        "email_updates": False,
        "cohort": "B",
    }

    def setUp(self) -> None:
        di_container = Container()
        self.civi = di_container.civi()
        # For testing custom_donation amounts at or just around minimum values
        # (because, again, data.custom_donation is an expression of dollars
        # and most stored money values are expressed as cents).
        # We ignore typing on these calls because Mypy gets confused by the conversion performed
        # by tordonate.utils.WithSyncMeta - which provides the below sync versions of async calls.
        # (This is not the only place where typing on these values is enforced, and if they are
        # incorrect then the build will not pass testing, so this is not as unsafe as it appears.)
        self.minimum_donation = self.civi.minimum_donation_sync() / 100  # type: ignore
        self.donation_perk_single_minimum = self.civi.get_active_perks_sync()[  # type: ignore
            0
        ].single_price / 100
        self.donation_perk_monthly_minimum = self.civi.get_active_perks_sync()[  # type: ignore
            0
        ].monthly_price / 100

    @mock.patch("altcha.verify_solution")
    def test_form_validation(self, mock_verify_solution: mock.Mock) -> None:
        mock_verify_solution.return_value = (True, None)
        form = DonorInfoFormB(
            data={
                **self.data,
                "altcha": make_valid_payload(),
            }
        )
        self.assertTrue(form.is_valid())

    def test_form_altcha_missing(self) -> None:
        form = DonorInfoFormB(data=self.data)
        self.assertFalse(form.is_valid())
        self.assertIsNotNone(form.errors.get("altcha"))

    def test_form_perk_missing(self) -> None:
        form_data = self.data.copy()
        del form_data["perk"]
        form = DonorInfoFormB(data=form_data)
        self.assertFalse(form.is_valid())

    @mock.patch("altcha.verify_solution")
    def test_form_invalid_email(self, mock_verify_solution: mock.Mock) -> None:
        mock_verify_solution.return_value = (True, None)
        form = DonorInfoFormB(
            data={
                **self.data,
                "altcha": make_valid_payload(),
                "email_address": "malformed@email",
            }
        )
        self.assertFalse(form.is_valid())
        self.assertIsNotNone(form.errors.get("__all__"))

    @mock.patch("altcha.verify_solution")
    def test_form_minimum_donation(self, mock_verify_solution: mock.Mock) -> None:
        mock_verify_solution.return_value = (True, None)
        form = DonorInfoFormB(
            data={
                **self.data,
                "altcha": make_valid_payload(),
                "custom_donation": self.minimum_donation,
            }
        )
        self.assertTrue(form.is_valid())
        self.assertIsNone(form.errors.get("__all__"))

    @mock.patch("altcha.verify_solution")
    def test_form_insufficient_donation_amount(self, mock_verify_solution: mock.Mock) -> None:
        mock_verify_solution.return_value = (True, None)
        form = DonorInfoFormB(
            data={
                **self.data,
                "altcha": make_valid_payload(),
                "custom_donation": self.minimum_donation - 1,
            }
        )
        self.assertFalse(form.is_valid())
        self.assertIsNotNone(form.errors.get("__all__"))

    @mock.patch("altcha.verify_solution")
    def test_form_invalid_donation_amount(self, mock_verify_solution: mock.Mock) -> None:
        mock_verify_solution.return_value = (True, None)
        form = DonorInfoFormB(
            data={
                **self.data,
                "altcha": make_valid_payload(),
                "custom_donation": "five dollars",
            }
        )
        self.assertFalse(form.is_valid())
        self.assertIsNotNone(form.errors.get("__all__"))

    @mock.patch("altcha.verify_solution")
    def test_form_valid_donation_amount_for_perk(self, mock_verify_solution: mock.Mock) -> None:
        mock_verify_solution.return_value = (True, None)
        form = DonorInfoFormB(
            data={
                **self.data,
                "altcha": make_valid_payload(),
                "custom_donation": self.donation_perk_single_minimum,
                "perk": self.civi.get_active_perks_sync()[0].id,  # type: ignore
            }
        )
        self.assertTrue(form.is_valid())
        self.assertIsNone(form.errors.get("__all__"))

    @mock.patch("altcha.verify_solution")
    def test_form_invalid_donation_amount_for_perk(self, mock_verify_solution: mock.Mock) -> None:
        mock_verify_solution.return_value = (True, None)
        form = DonorInfoFormB(
            data={
                **self.data,
                "altcha": make_valid_payload(),
                "custom_donation": self.donation_perk_single_minimum - 1,
                "perk": self.civi.get_active_perks_sync()[0].id,  # type: ignore
            }
        )
        self.assertFalse(form.is_valid())
        self.assertIsNotNone(form.errors.get("__all__"))

    @mock.patch("altcha.verify_solution")
    def test_form_invalid_perk(self, mock_verify_solution: mock.Mock) -> None:
        mock_verify_solution.return_value = (True, None)
        form = DonorInfoFormB(
            data={
                **self.data,
                "altcha": make_valid_payload(),
                "custom_donation": self.minimum_donation,
                "perk": "invalidperk",
            }
        )
        self.assertFalse(form.is_valid())
        self.assertIsNotNone(form.errors.get("perk"))

    @mock.patch("altcha.verify_solution")
    def test_form_monthly_donation(self, mock_verify_solution: mock.Mock) -> None:
        mock_verify_solution.return_value = (True, None)
        form = DonorInfoFormB(
            data={
                **self.data,
                "altcha": make_valid_payload(),
                "single_or_monthly": "monthly",
                "custom_donation":
                    self.donation_perk_monthly_minimum,
                "perk": self.civi.get_active_perks_sync()[0].id,  # type: ignore
            }
        )
        self.assertTrue(form.is_valid())
        self.assertIsNone(form.errors.get("perk"))

    @mock.patch("altcha.verify_solution")
    def test_form_insufficient_monthly_donation(self, mock_verify_solution: mock.Mock) -> None:
        mock_verify_solution.return_value = (True, None)
        form = DonorInfoFormB(
            data={
                **self.data,
                "altcha": make_valid_payload(),
                "single_or_monthly": "monthly",
                "custom_donation":
                    self.donation_perk_monthly_minimum - 1,
                "perk": self.civi.get_active_perks_sync()[0].id,  # type: ignore
            }
        )
        self.assertFalse(form.is_valid())
        self.assertIsNotNone(form.errors.get("__all__"))

    @mock.patch("altcha.verify_solution")
    def test_restricted_shipping(self, mock_verify_solution: mock.Mock) -> None:
        mock_verify_solution.return_value = (True, None)
        form = DonorInfoFormB(
            data={
                **self.data,
                "altcha": make_valid_payload(),
                "country": "Russia"
            }
        )
        self.assertFalse(form.is_valid())
        self.assertIsNotNone(form.errors.get("__all__"))
