"""Shop model ITs — Chunk 2 of the wallet-expansion sprint.

Pins:
  * `ShopItem` row shape (slug, name, prose, icon, badge, pricing,
    granted-token spec, max_owned, display_order, active).
  * `Purchase` row shape + the `fulfill()` method's idempotent token-
    mint + writs-grant semantics.
  * `ShopItem.is_available_for(user)` w/ `max_owned` enforcement.
  * Seeded catalog: `tithe-1`, `tithe-5`, `band-1` present + correctly
    configured per the locked decisions in [[project-wallet-shop-
    expansion]].
  * Seeded `wallet-shop` Applet present.
"""
from django.test import TestCase

from apps.applets.models import Applet
from apps.lyric.models import Purchase, ShopItem, Token, User


class ShopItemModelTest(TestCase):
    def test_create_shopitem_minimal(self):
        item = ShopItem.objects.create(
            slug="probe",
            name="Probe",
            price_cents=100,
            granted_token_type=Token.TITHE,
            granted_count=1,
        )
        self.assertEqual(item.slug, "probe")
        self.assertEqual(item.price_cents, 100)
        self.assertEqual(item.granted_count, 1)

    def test_granted_writs_defaults_to_zero(self):
        item = ShopItem.objects.create(
            slug="probe",
            name="Probe",
            price_cents=100,
            granted_token_type=Token.TITHE,
            granted_count=1,
        )
        self.assertEqual(item.granted_writs, 0)

    def test_max_owned_defaults_to_null(self):
        item = ShopItem.objects.create(
            slug="probe",
            name="Probe",
            price_cents=100,
            granted_token_type=Token.TITHE,
            granted_count=1,
        )
        self.assertIsNone(item.max_owned)

    def test_active_defaults_to_true(self):
        item = ShopItem.objects.create(
            slug="probe",
            name="Probe",
            price_cents=100,
            granted_token_type=Token.TITHE,
            granted_count=1,
        )
        self.assertTrue(item.active)

    def test_str_returns_name(self):
        item = ShopItem.objects.create(
            slug="probe", name="Wristband", price_cents=2000,
            granted_token_type=Token.BAND, granted_count=1,
        )
        self.assertEqual(str(item), "Wristband")

    def test_shoptalk_defaults_to_blank(self):
        """Most items have no shoptalk (italic flavor line) — defaults to ""
        so the tooltip's `{% if item.shoptalk %}` skips the slot cleanly."""
        item = ShopItem.objects.create(
            slug="probe", name="Probe", price_cents=100,
            granted_token_type=Token.TITHE, granted_count=1,
        )
        self.assertEqual(item.shoptalk, "")

    def test_tooltip_expiry_returns_no_expiry(self):
        """All shop items are eternal stock — the tooltip's expiry slot
        always reads 'no expiry' (matches PASS/BAND/CARTE token tooltips,
        DRY-reuses `.tt-expiry` SCSS for the red callout)."""
        item = ShopItem.objects.create(
            slug="probe", name="Probe", price_cents=100,
            granted_token_type=Token.TITHE, granted_count=1,
        )
        self.assertEqual(item.tooltip_expiry(), "no expiry")

    def test_is_available_for_unlimited_item(self):
        """`max_owned=None` → item is always available."""
        item = ShopItem.objects.create(
            slug="probe", name="Tithe", price_cents=100,
            granted_token_type=Token.TITHE, granted_count=1,
        )
        user = User.objects.create(email="ok@test.io")
        self.assertTrue(item.is_available_for(user))

    def test_is_available_for_when_under_max_owned(self):
        """`max_owned=1` + user owns 0 → available."""
        item = ShopItem.objects.create(
            slug="band", name="Wristband", price_cents=2000,
            granted_token_type=Token.BAND, granted_count=1, max_owned=1,
        )
        user = User.objects.create(email="ok@test.io")
        self.assertTrue(item.is_available_for(user))

    def test_is_not_available_for_at_max_owned(self):
        """`max_owned=1` + user already owns 1 BAND → not available."""
        item = ShopItem.objects.create(
            slug="band", name="Wristband", price_cents=2000,
            granted_token_type=Token.BAND, granted_count=1, max_owned=1,
        )
        user = User.objects.create(email="bandowner@test.io")
        Token.objects.create(user=user, token_type=Token.BAND)
        self.assertFalse(item.is_available_for(user))


class PurchaseModelTest(TestCase):
    def setUp(self):
        self.user = User.objects.create(email="buyer@test.io")
        # Drop the auto-FREE so the FREE-token count assertions stay tight.
        self.user.tokens.all().delete()
        self.user.refresh_from_db()
        self.tithe_item = ShopItem.objects.create(
            slug="probe-tithe", name="Tithe", price_cents=100,
            granted_token_type=Token.TITHE, granted_count=1, granted_writs=144,
        )

    def test_create_purchase_minimal(self):
        purchase = Purchase.objects.create(
            user=self.user,
            shop_item=self.tithe_item,
            stripe_payment_intent_id="pi_test_123",
            amount_cents=100,
            granted_writs=144,
        )
        self.assertEqual(purchase.user, self.user)
        self.assertEqual(purchase.shop_item, self.tithe_item)
        self.assertEqual(purchase.status, Purchase.PENDING)
        self.assertEqual(purchase.granted_token_ids, [])
        self.assertIsNone(purchase.succeeded_at)

    def test_stripe_payment_intent_id_is_unique(self):
        Purchase.objects.create(
            user=self.user, shop_item=self.tithe_item,
            stripe_payment_intent_id="pi_dup", amount_cents=100, granted_writs=144,
        )
        with self.assertRaises(Exception):
            Purchase.objects.create(
                user=self.user, shop_item=self.tithe_item,
                stripe_payment_intent_id="pi_dup",
                amount_cents=100, granted_writs=144,
            )

    def test_fulfill_mints_tokens(self):
        purchase = Purchase.objects.create(
            user=self.user, shop_item=self.tithe_item,
            stripe_payment_intent_id="pi_fulfill_1",
            amount_cents=100, granted_writs=144,
        )
        purchase.fulfill()
        self.assertEqual(
            self.user.tokens.filter(token_type=Token.TITHE).count(), 1
        )

    def test_fulfill_grants_writs(self):
        purchase = Purchase.objects.create(
            user=self.user, shop_item=self.tithe_item,
            stripe_payment_intent_id="pi_fulfill_2",
            amount_cents=100, granted_writs=144,
        )
        before = self.user.wallet.writs
        purchase.fulfill()
        self.user.wallet.refresh_from_db()
        self.assertEqual(self.user.wallet.writs, before + 144)

    def test_fulfill_marks_succeeded(self):
        purchase = Purchase.objects.create(
            user=self.user, shop_item=self.tithe_item,
            stripe_payment_intent_id="pi_fulfill_3",
            amount_cents=100, granted_writs=144,
        )
        purchase.fulfill()
        purchase.refresh_from_db()
        self.assertEqual(purchase.status, Purchase.SUCCEEDED)
        self.assertIsNotNone(purchase.succeeded_at)

    def test_fulfill_records_granted_token_ids(self):
        purchase = Purchase.objects.create(
            user=self.user, shop_item=self.tithe_item,
            stripe_payment_intent_id="pi_fulfill_4",
            amount_cents=100, granted_writs=144,
        )
        purchase.fulfill()
        purchase.refresh_from_db()
        tithe = self.user.tokens.filter(token_type=Token.TITHE).first()
        self.assertIn(tithe.pk, purchase.granted_token_ids)

    def test_fulfill_is_idempotent(self):
        """Webhook + sync-confirm race could fire `fulfill()` twice — second
        call must be a no-op (no double-mint, no double-writs)."""
        purchase = Purchase.objects.create(
            user=self.user, shop_item=self.tithe_item,
            stripe_payment_intent_id="pi_idem",
            amount_cents=100, granted_writs=144,
        )
        purchase.fulfill()
        first_writs = self.user.wallet.refresh_from_db() or self.user.wallet.writs
        purchase.fulfill()
        self.user.wallet.refresh_from_db()
        self.assertEqual(self.user.wallet.writs, first_writs)
        self.assertEqual(
            self.user.tokens.filter(token_type=Token.TITHE).count(), 1
        )

    def test_fulfill_creates_multiple_tokens_for_bundle(self):
        bundle = ShopItem.objects.create(
            slug="probe-bundle", name="Tithe ×5", price_cents=400,
            granted_token_type=Token.TITHE, granted_count=5, granted_writs=750,
        )
        purchase = Purchase.objects.create(
            user=self.user, shop_item=bundle,
            stripe_payment_intent_id="pi_bundle",
            amount_cents=400, granted_writs=750,
        )
        purchase.fulfill()
        self.assertEqual(
            self.user.tokens.filter(token_type=Token.TITHE).count(), 5
        )


class SeededShopCatalogTest(TestCase):
    """Migration `lyric/0008_seed_shop_items` seeds the 3 starting items.
    Pinned here so a future migration that touches the table can't quietly
    drop / rename / re-price them without a deliberate update to these
    assertions."""

    def test_tithe_one_item_present(self):
        item = ShopItem.objects.get(slug="tithe-1")
        self.assertEqual(item.price_cents, 100)
        self.assertEqual(item.granted_token_type, Token.TITHE)
        self.assertEqual(item.granted_count, 1)
        # Re-balanced 2026-05-22 (migration `0010_repricing_tithe_writs`):
        # 144 → 12 writs per tithe-1 purchase.
        self.assertEqual(item.granted_writs, 12)
        self.assertIsNone(item.max_owned)

    def test_tithe_five_bundle_item_present(self):
        item = ShopItem.objects.get(slug="tithe-5")
        self.assertEqual(item.price_cents, 400)
        self.assertEqual(item.granted_count, 5)
        # Re-balanced 2026-05-22: 750 → 60 writs per bundle purchase.
        self.assertEqual(item.granted_writs, 60)
        self.assertEqual(item.badge_text, "×5")

    def test_band_item_present(self):
        item = ShopItem.objects.get(slug="band-1")
        self.assertEqual(item.price_cents, 2000)
        self.assertEqual(item.granted_token_type, Token.BAND)
        self.assertEqual(item.granted_count, 1)
        self.assertEqual(item.max_owned, 1)
        self.assertEqual(item.granted_writs, 0)
        # BAND carries the italic shoptalk line from the Token tooltip —
        # DRY w. `Token.tooltip_shoptalk` for the BAND type.
        self.assertEqual(item.shoptalk, "Unlimited free entry (BYOB)")

    def test_tithe_items_have_no_shoptalk(self):
        """Tithes don't carry italic flavor in the Token tooltip; shop
        mirrors that — `.tt-shoptalk` slot is empty + the template's
        `{% if %}` skips the line cleanly."""
        self.assertEqual(ShopItem.objects.get(slug="tithe-1").shoptalk, "")
        self.assertEqual(ShopItem.objects.get(slug="tithe-5").shoptalk, "")

    def test_all_three_items_active(self):
        for slug in ("tithe-1", "tithe-5", "band-1"):
            self.assertTrue(
                ShopItem.objects.get(slug=slug).active,
                msg=f"{slug} should be active",
            )

    def test_display_order_ascends(self):
        """Catalog display order matches the spec: tithe-1 < tithe-5 < band-1."""
        slugs = list(
            ShopItem.objects.order_by("display_order").values_list("slug", flat=True)
        )
        # filter to the seeded ones (in case future migrations add more)
        seeded = [s for s in slugs if s in ("tithe-1", "tithe-5", "band-1")]
        self.assertEqual(seeded, ["tithe-1", "tithe-5", "band-1"])


class SeededWalletShopAppletTest(TestCase):
    def test_wallet_shop_applet_present(self):
        applet = Applet.objects.get(slug="wallet-shop")
        self.assertEqual(applet.context, Applet.WALLET)
        self.assertEqual(applet.grid_cols, 12)
