import uuid
from django.contrib import auth
from django.test import TestCase
from django.utils import timezone

from apps.drama.models import Note
from apps.lyric.models import LoginToken, PaymentMethod, Token, User, Wallet


class UserModelTest(TestCase):
    def test_model_is_configured_for_django_auth(self):
        self.assertEqual(auth.get_user_model(), User)
    
    def test_user_is_valid_with_email_only(self):
        user = User(email="a@b.cde")
        user.set_unusable_password()
        user.full_clean() # should not raise

    def test_id_is_primary_key(self):
        user = User(id="123")
        self.assertEqual(user.pk, "123")

    def test_user_can_have_a_username(self):
        user = User.objects.create(email="a@b.cde")
        user.username = "stardust"
        user.save()
        self.assertEqual(User.objects.get(pk=user.pk).username, "stardust")

    def test_searchable_defaults_to_false(self):
        user = User.objects.create(email="a@b.cde")
        self.assertFalse(user.searchable)

    def test_active_title_is_null_by_default(self):
        user = User.objects.create(email="a@b.cde")
        self.assertIsNone(user.active_title)

    def test_active_title_can_be_set_to_a_note(self):
        user = User.objects.create(email="a@b.cde")
        note = Note.objects.create(user=user, slug="stargazer", earned_at=timezone.now())
        user.active_title = note
        user.save(update_fields=["active_title"])
        user.refresh_from_db()
        self.assertEqual(user.active_title, note)

    def test_clearing_note_nullifies_active_title(self):
        user = User.objects.create(email="a@b.cde")
        note = Note.objects.create(user=user, slug="stargazer", earned_at=timezone.now())
        user.active_title = note
        user.save(update_fields=["active_title"])
        note.delete()
        user.refresh_from_db()
        self.assertIsNone(user.active_title)

    def test_active_title_display_returns_earthman_when_no_note_donned(self):
        user = User.objects.create(email="a@b.cde")
        self.assertEqual(user.active_title_display, "Earthman")

    def test_active_title_display_uses_display_name_not_display_title(self):
        """Inline attributions like `.post-attribution` should render the
        Note's `display_name`, not its `display_title` — the only divergence
        today is Baltimorean (display_title="Ard!" navbar-flair, display_name
        ="Baltimorean" everywhere else). Pinning here so future Notes that
        override one field but not the other don't surprise the post page."""
        user = User.objects.create(email="ard@b.cde")
        note = Note.objects.create(user=user, slug="baltimorean", earned_at=timezone.now())
        user.active_title = note
        user.save(update_fields=["active_title"])
        self.assertEqual(user.active_title_display, "Baltimorean")

    def test_active_title_display_falls_through_to_display_title_for_non_overridden_slugs(self):
        """Stargazer / Schizo / Nomad don't override display_name, so
        active_title_display returns the same string display_title would —
        the rename pattern is no-op for these surfaces."""
        user = User.objects.create(email="star@b.cde")
        note = Note.objects.create(user=user, slug="stargazer", earned_at=timezone.now())
        user.active_title = note
        user.save(update_fields=["active_title"])
        self.assertEqual(user.active_title_display, note.display_title)

class LoginTokenModelTest(TestCase):
    def test_links_user_with_autogen_uid(self):
        login_token1 = LoginToken.objects.create(email="a@b.cde")
        login_token2 = LoginToken.objects.create(email="v@w.xyz")
        self.assertNotEqual(login_token1.pk, login_token2.pk)
        self.assertIsInstance(login_token1.pk, uuid.UUID)

class UserManagerTest(TestCase):
    def test_create_superuser_sets_is_staff_and_is_superuser(self):
        user = User.objects.create_superuser(
            email="admin@example.com",
            password="correct-password",
        )
        self.assertTrue(user.is_staff)
        self.assertTrue(user.is_superuser)

    def test_create_superuser_sets_usable_password(self):
        user = User.objects.create_superuser(
            email="admin@example.com",
            password="correct-password",
        )
        self.assertTrue(user.check_password("correct-password"))

class UserPaletteTest(TestCase):
    def test_palette_field_defaults_to_palette_default(self):
        user = User.objects.create(email="a@b.cde")
        self.assertEqual(user.palette, "palette-default")


class UserPronounsTest(TestCase):
    def test_pronouns_default_to_pluralism(self):
        user = User.objects.create(email="they@test.io")
        self.assertEqual(user.pronouns, "pluralism")

    def test_pronouns_choices_include_all_five_options(self):
        from apps.lyric.models import PRONOUN_CHOICES
        keys = [k for (k, _) in PRONOUN_CHOICES]
        self.assertEqual(
            keys,
            ["pluralism", "bawlmorese", "misogyny", "misandry", "misanthropy"],
        )

    def test_pluralism_resolves_to_they_them_their(self):
        user = User.objects.create(email="plural@test.io")
        self.assertEqual(user.pronoun_subj, "they")
        self.assertEqual(user.pronoun_obj,  "them")
        self.assertEqual(user.pronoun_poss, "their")

    def test_bawlmorese_resolves_to_yo_yo_yos(self):
        user = User.objects.create(email="yo@test.io", pronouns="bawlmorese")
        self.assertEqual(user.pronoun_subj, "yo")
        self.assertEqual(user.pronoun_obj,  "yo")
        self.assertEqual(user.pronoun_poss, "yos")

    def test_misogyny_resolves_to_he_him_his(self):
        user = User.objects.create(email="he@test.io", pronouns="misogyny")
        self.assertEqual(user.pronoun_subj, "he")
        self.assertEqual(user.pronoun_obj,  "him")
        self.assertEqual(user.pronoun_poss, "his")

    def test_misandry_resolves_to_she_her_hers(self):
        user = User.objects.create(email="she@test.io", pronouns="misandry")
        self.assertEqual(user.pronoun_subj, "she")
        self.assertEqual(user.pronoun_obj,  "her")
        self.assertEqual(user.pronoun_poss, "hers")

    def test_misanthropy_resolves_to_it_it_its(self):
        user = User.objects.create(email="it@test.io", pronouns="misanthropy")
        self.assertEqual(user.pronoun_subj, "it")
        self.assertEqual(user.pronoun_obj,  "it")
        self.assertEqual(user.pronoun_poss, "its")

class WalletCreationTest(TestCase):
    def setUp(self):
        self.user = User.objects.create(email="capman@test.io")

    def test_wallet_is_created_for_new_user(self):
        self.assertTrue(Wallet.objects.filter(user=self.user).exists())
    
    def test_new_wallet_has_144_writs(self):
        wallet = Wallet.objects.get(user = self.user)
        self.assertEqual(wallet.writs, 144)

    def test_new_wallet_has_0_esteem(self):
        wallet = Wallet.objects.get(user=self.user)
        self.assertEqual(wallet.esteem, 0)

class TokenCreationTest(TestCase):
    def setUp(self):
        self.user = User.objects.create(email="capman@test.io")

    def test_coin_on_a_string_created_for_new_user(self):
        self.assertTrue(
            Token.objects.filter(user=self.user, token_type=Token.COIN).exists()
        )

    def test_free_token_created_for_new_user(self):
        self.assertTrue(
            Token.objects.filter(user=self.user, token_type=Token.FREE).exists()
        )

    def test_coin_on_a_string_has_no_expiry(self):
        coin = Token.objects.get(user=self.user, token_type=Token.COIN)
        self.assertIsNone(coin.expires_at)

    def test_free_token_has_expiry_within_7_days(self):
        free = Token.objects.get(user=self.user, token_type=Token.FREE)
        self.assertIsNotNone(free.expires_at)
        delta = free.expires_at - timezone.now()
        self.assertLessEqual(delta.days, 7)
        self.assertGreater(delta.total_seconds(), 0)

    def test_no_pass_token_for_regular_user(self):
        self.assertFalse(
            Token.objects.filter(user=self.user, token_type=Token.PASS).exists()
        )

class SuperuserTokenCreationTest(TestCase):
    def setUp(self):
        self.user = User.objects.create_superuser(
            email="admin@test.io", password="secret"
        )

    def test_pass_token_created_for_superuser(self):
        self.assertTrue(
            Token.objects.filter(user=self.user, token_type=Token.PASS).exists()
        )

    def test_superuser_also_gets_coin_and_free_token(self):
        self.assertTrue(
            Token.objects.filter(user=self.user, token_type=Token.COIN).exists()
        )
        self.assertTrue(
            Token.objects.filter(user=self.user, token_type=Token.FREE).exists()
        )


class SuperuserNoteGrantTest(TestCase):
    def setUp(self):
        self.user = User.objects.create_superuser(
            email="admin@test.io", password="secret"
        )

    def test_superuser_gets_super_schizo_note(self):
        from apps.drama.models import Note
        self.assertTrue(Note.objects.filter(user=self.user, slug="super-schizo").exists())

    def test_superuser_gets_super_nomad_note(self):
        from apps.drama.models import Note
        self.assertTrue(Note.objects.filter(user=self.user, slug="super-nomad").exists())

    def test_regular_user_does_not_get_super_notes(self):
        from apps.drama.models import Note
        regular = User.objects.create(email="regular@test.io")
        self.assertFalse(Note.objects.filter(user=regular, slug="super-schizo").exists())
        self.assertFalse(Note.objects.filter(user=regular, slug="super-nomad").exists())


class WalletTooltipTest(TestCase):
    def setUp(self):
        self.user = User.objects.create(email="wallet@test.io")
        self.wallet = Wallet.objects.get(user=self.user)

    def test_tooltip_name(self):
        self.assertEqual(self.wallet.tooltip_name(), "Wallet")

    def test_tooltip_description(self):
        self.wallet.writs = 144
        self.wallet.esteem = 12
        self.assertEqual(self.wallet.tooltip_description(), "144 writs · 12 esteem")

    def test_tooltip_shoptalk_returns_none(self):
        self.assertIsNone(self.wallet.tooltip_shoptalk())

    def test_tooltip_expiry_returns_none(self):
        self.assertIsNone(self.wallet.tooltip_expiry())

    def test_tooltip_text(self):
        self.assertEqual(self.wallet.tooltip_text(), "Wallet: 144 writs · 0 esteem")


class TokenTooltipTest(TestCase):
    def setUp(self):
        self.user = User.objects.create(email="tokens@test.io")

    def test_tithe_tooltip_description(self):
        tithe = Token.objects.create(user=self.user, token_type=Token.TITHE)
        self.assertEqual(tithe.tooltip_description(), "+ Writ bonus")

    def test_tooltip_description_empty_fallback(self):
        # token_type other than COIN/FREE/TITHE hits the bare return ""
        token = Token(user=self.user, token_type="")
        self.assertEqual(token.tooltip_description(), "")

    def test_tooltip_expiry_empty_when_no_expiry_and_not_coin(self):
        free = Token.objects.get(user=self.user, token_type=Token.FREE)
        free.expires_at = None
        self.assertEqual(free.tooltip_expiry(), "")

    def test_tooltip_shoptalk_none_for_free_coin(self):
        free = Token.objects.get(user=self.user, token_type=Token.FREE)
        self.assertEqual(free.tooltip_shoptalk(), "a spot of good fortune")

    def test_tooltip_room_html_returns_empty_when_no_room(self):
        token = Token.objects.get(user=self.user, token_type=Token.COIN)
        self.assertEqual(token.tooltip_room_html(), "")


class EquippedTrinketTest(TestCase):
    def setUp(self):
        self.user = User.objects.create(email="equip@test.io", is_staff=True)
        self.pass_token = self.user.tokens.get(token_type=Token.PASS)

    def test_normal_user_equipped_trinket_defaults_to_coin(self):
        user = User.objects.create(email="noequip@test.io")
        coin = user.tokens.get(token_type=Token.COIN)
        self.assertEqual(user.equipped_trinket, coin)

    def test_staff_user_equipped_trinket_defaults_to_pass(self):
        self.assertEqual(self.user.equipped_trinket, self.pass_token)

    def test_equipped_trinket_can_be_set_to_pass(self):
        self.user.equipped_trinket = self.pass_token
        self.user.save(update_fields=["equipped_trinket"])
        self.assertEqual(
            User.objects.get(pk=self.user.pk).equipped_trinket, self.pass_token
        )

    def test_equipped_trinket_can_be_set_to_carte(self):
        carte = Token.objects.create(user=self.user, token_type=Token.CARTE)
        self.user.equipped_trinket = carte
        self.user.save(update_fields=["equipped_trinket"])
        self.assertEqual(
            User.objects.get(pk=self.user.pk).equipped_trinket, carte
        )

    def test_equipped_trinket_can_be_cleared(self):
        self.user.equipped_trinket = self.pass_token
        self.user.save(update_fields=["equipped_trinket"])
        self.user.equipped_trinket = None
        self.user.save(update_fields=["equipped_trinket"])
        self.assertIsNone(User.objects.get(pk=self.user.pk).equipped_trinket)


class CarteTokenCreationTest(TestCase):
    def setUp(self):
        self.user = User.objects.create(email="carte@test.io")

    def test_carte_token_can_be_created(self):
        token = Token.objects.create(user=self.user, token_type=Token.CARTE)
        self.assertEqual(Token.objects.get(pk=token.pk).token_type, Token.CARTE)

    def test_carte_has_no_expiry(self):
        token = Token.objects.create(user=self.user, token_type=Token.CARTE)
        self.assertIsNone(token.expires_at)


class PassTokenStaffOnlyGuardTest(TestCase):
    """PASS is the admin-only trinket. Creating one for a non-staff user
    must raise — guard the rule at the model layer so admin UI, shell,
    and any future programmatic path all enforce it. See [[feedback-pass-token-staff-only]]
    for the broader design (a non-admin variant lands as a distinct token later)."""

    def test_create_pass_for_non_staff_raises(self):
        from django.core.exceptions import ValidationError
        non_staff = User.objects.create(email="gamer@test.io")
        with self.assertRaises(ValidationError):
            Token.objects.create(user=non_staff, token_type=Token.PASS)

    def test_create_pass_for_staff_succeeds(self):
        staff = User.objects.create(email="admin@test.io", is_staff=True)
        # superuser auto-grants a PASS in the post_save signal; delete it so
        # we can assert the explicit-create path works without UNIQUE issues.
        Token.objects.filter(user=staff, token_type=Token.PASS).delete()
        token = Token.objects.create(user=staff, token_type=Token.PASS)
        self.assertEqual(token.token_type, Token.PASS)

    def test_non_pass_token_for_non_staff_still_allowed(self):
        non_staff = User.objects.create(email="ok@test.io")
        token = Token.objects.create(user=non_staff, token_type=Token.TITHE)
        self.assertEqual(token.token_type, Token.TITHE)


class BandTokenAdminAwardTest(TestCase):
    """BAND is the non-admin variant of PASS. Contract: NOT auto-granted by
    the post_save signal (unlike PASS for staff, or COIN/FREE for everyone),
    so admin can hand-mint it for any user via the Django admin without
    every fresh account spawning one. No `is_staff` coupling — staff or
    non-staff can hold one — so no model-layer guard either."""

    def test_band_not_auto_granted_on_user_creation(self):
        user = User.objects.create(email="newgamer@test.io")
        self.assertFalse(user.tokens.filter(token_type=Token.BAND).exists())

    def test_band_not_auto_granted_on_staff_creation(self):
        """Staff get PASS, not BAND, on creation. BAND is admin-awarded
        post-hoc to either staff or non-staff (no role-coupling like PASS)."""
        staff = User.objects.create(email="admin@test.io", is_staff=True)
        self.assertFalse(staff.tokens.filter(token_type=Token.BAND).exists())

    def test_admin_can_award_band_to_non_staff(self):
        non_staff = User.objects.create(email="gamer@test.io")
        token = Token.objects.create(user=non_staff, token_type=Token.BAND)
        self.assertEqual(token.token_type, Token.BAND)

    def test_admin_can_award_band_to_staff(self):
        staff = User.objects.create(email="admin@test.io", is_staff=True)
        token = Token.objects.create(user=staff, token_type=Token.BAND)
        self.assertEqual(token.token_type, Token.BAND)

    def test_band_not_auto_equipped_after_award(self):
        """Award diverges from PASS — admin-minted BAND lands in the wallet
        but the user must DON it manually (`equipped_trinket` stays at its
        post-signal default of COIN for non-staff)."""
        non_staff = User.objects.create(email="bro@test.io")
        coin = non_staff.tokens.get(token_type=Token.COIN)
        band = Token.objects.create(user=non_staff, token_type=Token.BAND)
        non_staff.refresh_from_db()
        self.assertEqual(non_staff.equipped_trinket, coin)
        self.assertNotEqual(non_staff.equipped_trinket, band)


class EquippedDeckTest(TestCase):
    def test_new_user_gets_earthman_as_default_deck(self):
        from apps.epic.models import DeckVariant
        earthman = DeckVariant.objects.get(slug="earthman")
        user = User.objects.create(email="deck@test.io")
        user.refresh_from_db()
        self.assertEqual(user.equipped_deck, earthman)

    def test_fiorentine_is_not_auto_assigned_to_new_users(self):
        from apps.epic.models import DeckVariant
        fiorentine = DeckVariant.objects.get(slug="tarot-rider-waite-smith")
        user = User.objects.create(email="deck2@test.io")
        user.refresh_from_db()
        self.assertNotEqual(user.equipped_deck, fiorentine)

    def test_equipped_deck_can_be_switched(self):
        from apps.epic.models import DeckVariant
        fiorentine = DeckVariant.objects.get(slug="tarot-rider-waite-smith")
        user = User.objects.create(email="deck3@test.io")
        user.equipped_deck = fiorentine
        user.save(update_fields=["equipped_deck"])
        self.assertEqual(User.objects.get(pk=user.pk).equipped_deck, fiorentine)


class PaymentMethodTest(TestCase):
    def setUp(self):
        self.user = User.objects.create(email="pay@test.io")

    def test_str(self):
        pm = PaymentMethod(user=self.user, stripe_pm_id="pm_123", last4="4242", brand="Visa")
        self.assertEqual(str(pm), "Visa ....4242")
