"""Functional tests for the Baltimorean Note unlock loop.

Mirrors the Stargazer unlock pattern (see test_bill_my_notes.py) but the
trigger lives on Game Kit's Pronouns applet instead of the My Sky save —
selecting `bawlmorese` as the global pronoun for the first time grants the
Baltimorean Note and fires the slide-down Brief banner.

End-to-end loop captured here:
  1. Game Kit → pronouns applet → click `bawlmorese` card → guard portal "OK"
  2. Brief banner slides in with title "Baltimorean" + Look! prose + FYI/NVM/?-square
  3. Navigate to /billboard/my-notes/ → Baltimorean item w. the Aaron quote
  4. DON the Baltimorean Note → navbar greeting flips
     "Welcome, Earthman" → "Ayo, Ard!"
  5. Click the ? swatch box → modal opens w. Baltimore + Maryland swatches
  6. Click a Baltimore swatch body → body class previews palette-baltimore
  7. Click OK in the confirm bar → palette commits (Note.palette saves,
     user.palette saves, ? box replaced by palette swatch)
  8. Navigate back to /dashboard/ → Palette applet shows Baltimore swatch
     unlocked + data-description carrying "Baltimorean" attribution

Each scenario is its own test method so a regression in any segment fails
in isolation (and the slowest segments — Game Kit pronouns flip vs palette
modal commit — run independently in --parallel).
"""
import time

from django.test import tag
from django.utils import timezone
from selenium.webdriver.common.by import By

from apps.applets.models import Applet
from apps.drama.models import Note
from apps.lyric.models import User

from .base import FunctionalTest


def _setup_applets():
    """Both surfaces this loop visits need their applets registered."""
    Applet.objects.get_or_create(
        slug="pronouns",
        defaults={"name": "Pronouns", "grid_cols": 6, "grid_rows": 6, "context": "game-kit"},
    )
    Applet.objects.get_or_create(
        slug="palette",
        defaults={"name": "Palette", "grid_cols": 6, "grid_rows": 6, "context": "dashboard"},
    )
    Applet.objects.get_or_create(
        slug="notes",
        defaults={"name": "Note", "grid_cols": 4, "grid_rows": 4, "context": "billboard"},
    )


class BaltimoreanNoteFromGameKitTest(FunctionalTest):
    """The full Baltimorean unlock loop. Setup creates a fresh gamer; each
    test seeds either nothing (the unlock-moment tests) or a pre-granted
    Note (the post-unlock tests) so the surfaces under test are isolated."""

    def setUp(self):
        super().setUp()
        # Portrait viewport — kit dialog + my-notes layouts both target the
        # narrow layout in their respective FT suites (mirrors test_dash_palette
        # + test_bill_my_notes conventions). 800x1200 keeps the navbar h1 +
        # guard portal + brief banner all in-frame.
        self.browser.set_window_size(800, 1200)
        _setup_applets()
        self.gamer = User.objects.create(email="ard@bawlmore.io")
        self.game_kit_url = self.live_server_url + "/gameboard/game-kit/"
        self.my_notes_url = self.live_server_url + "/billboard/my-notes/"

    # ── helpers ──────────────────────────────────────────────────────────

    def _click_bawlmorese_with_guard_ok(self):
        """Game Kit pronouns flow: click bawlmorese card → guard "OK" → commit.
        Uses execute_script for the OK click to bypass any in-flight scroll-
        into-view contention (same pattern as test_core_bud_btn helpers)."""
        card = self.wait_for(lambda: self.browser.find_element(
            By.CSS_SELECTOR, '.gk-pronoun-card[data-pronoun="bawlmorese"]'
        ))
        card.click()
        guard_ok = self.wait_for(lambda: self.browser.find_element(
            By.CSS_SELECTOR, "#id_guard_portal.active .guard-yes"
        ))
        self.browser.execute_script(
            "arguments[0].dispatchEvent(new MouseEvent('click', {bubbles: true}))",
            guard_ok,
        )

    # ── T1 — Game-Kit pronouns click fires the Brief banner ─────────────

    def test_first_bawlmorese_click_fires_baltimorean_brief_banner(self):
        """First `bawlmorese` selection from Game Kit's Pronouns applet must
        slide in a Brief banner carrying the Baltimorean title + a square that
        jumps to my-notes. The FYI button navigates to the underlying Post."""
        self.create_pre_authenticated_session("ard@bawlmore.io")
        self.browser.get(self.game_kit_url)

        # No banner on the cold page
        self.assertFalse(
            self.browser.find_elements(By.CSS_SELECTOR, ".note-banner"),
            "Brief banner must not be present before the unlock click",
        )

        self._click_bawlmorese_with_guard_ok()

        banner = self.wait_for_slow(lambda: self.browser.find_element(
            By.CSS_SELECTOR, ".note-banner"
        ))
        # The title slot carries the Note name — "Baltimorean". The don-able
        # title "Ard!" only surfaces in the navbar greeting after DON.
        self.assertIn(
            "Baltimorean",
            banner.find_element(By.CSS_SELECTOR, ".note-banner__title").text,
        )
        # Look! prose lands in the description.
        desc_text = banner.find_element(
            By.CSS_SELECTOR, ".note-banner__description"
        ).text
        self.assertIn("Look!", desc_text)
        # Banner gets a timestamp + the ? square anchored to /billboard/my-notes/.
        banner.find_element(By.CSS_SELECTOR, ".note-banner__timestamp")
        square = banner.find_element(By.CSS_SELECTOR, ".note-banner__image")
        self.assertEqual(square.tag_name, "a")
        self.assertEqual(
            square.get_attribute("href").rstrip("/"),
            self.live_server_url + "/billboard/my-notes",
        )
        # FYI navigates to the Brief's underlying Post detail.
        fyi = banner.find_element(By.CSS_SELECTOR, ".btn.btn-info")
        fyi.click()
        self.wait_for(lambda: self.assertRegex(
            self.browser.current_url, r"/billboard/post/[0-9a-f-]+/"
        ))

    # ── T2 — Idempotence: re-selecting bawlmorese does NOT re-fire banner

    def test_second_bawlmorese_click_does_not_re_fire_banner(self):
        """The Baltimorean Note grant is idempotent — clicking bawlmorese
        again after it's already the user's pronouns + Note is granted must
        not slide in another banner."""
        # Pre-grant — short-circuits the Brief on the subsequent click.
        Note.objects.create(
            user=self.gamer, slug="baltimorean", earned_at=timezone.now(),
        )
        self.gamer.pronouns = "bawlmorese"
        self.gamer.save()

        self.create_pre_authenticated_session("ard@bawlmore.io")
        self.browser.get(self.game_kit_url)
        self._click_bawlmorese_with_guard_ok()
        # The commit reloads — wait for the page to settle, then assert
        # no banner. Give the reload + render a beat.
        time.sleep(1.2)
        self.assertFalse(
            self.browser.find_elements(By.CSS_SELECTOR, ".note-banner"),
            "Repeat bawlmorese click must not re-fire the Brief banner",
        )

    # ── T3 — my-notes page renders the Baltimorean item w. the Aaron quote

    def test_my_notes_baltimorean_item_renders_aaron_quote(self):
        """Once the Note exists, /billboard/my-notes/ shows a Baltimorean
        item whose description is literally the Aaron quote (the central
        joke of the Note)."""
        Note.objects.create(
            user=self.gamer, slug="baltimorean", earned_at=timezone.now(),
        )
        self.create_pre_authenticated_session("ard@bawlmore.io")
        self.browser.get(self.my_notes_url)

        item = self.wait_for(lambda: self.browser.find_element(
            By.CSS_SELECTOR, '.note-list .note-item[data-slug="baltimorean"]'
        ))
        self.assertIn(
            "Baltimorean",
            item.find_element(By.CSS_SELECTOR, ".note-item__title").text,
        )
        # The Aaron quote is the description verbatim, including the
        # surrounding double-quotes that the user spec'd.
        self.assertIn(
            '"Aaron earned an iron urn."',
            item.find_element(By.CSS_SELECTOR, ".note-item__description").text,
        )

    # ── T4 — DON flips the navbar greeting to "Ayo, Ard!"

    def test_don_baltimorean_changes_navbar_greeting(self):
        """Pressing DON on the Baltimorean note item rewrites the navbar h1
        greeting from "Welcome, Earthman" to "Ayo, Ard!"."""
        Note.objects.create(
            user=self.gamer, slug="baltimorean", earned_at=timezone.now(),
        )
        self.create_pre_authenticated_session("ard@bawlmore.io")
        self.browser.get(self.my_notes_url)

        # Pre-condition: cold navbar greeting
        prefix = self.browser.find_element(By.ID, "id_greeting_prefix")
        name   = self.browser.find_element(By.ID, "id_greeting_name")
        self.assertEqual(prefix.text.strip().rstrip(","), "Welcome")
        self.assertEqual(name.text.strip(), "Earthman")

        # Click-lock the item first (the note-page locks on a click before
        # the DON button becomes interactive — mirrors the test_bill_my_notes
        # opacity:0 pattern).
        item = self.browser.find_element(
            By.CSS_SELECTOR, '.note-item[data-slug="baltimorean"]'
        )
        item.click()
        don = item.find_element(By.CSS_SELECTOR, ".note-don-btn")
        self.browser.execute_script(
            "arguments[0].dispatchEvent(new MouseEvent('click', {bubbles: true}))",
            don,
        )

        # The greeting flips client-side via the don_title JSON response.
        self.wait_for(lambda: self.assertEqual(
            self.browser.find_element(By.ID, "id_greeting_prefix").text.strip().rstrip(","),
            "Ayo",
        ))
        self.assertEqual(
            self.browser.find_element(By.ID, "id_greeting_name").text.strip(),
            "Ard!",
        )

    # ── T5 — palette modal opens to Baltimore + Maryland swatches

    def test_palette_modal_shows_baltimore_and_maryland_swatches(self):
        """The ? swatch box opens a modal carrying exactly the two
        palettes Baltimorean unlocks: Baltimore + Maryland."""
        Note.objects.create(
            user=self.gamer, slug="baltimorean", earned_at=timezone.now(),
        )
        self.create_pre_authenticated_session("ard@bawlmore.io")
        self.browser.get(self.my_notes_url)

        item = self.wait_for(lambda: self.browser.find_element(
            By.CSS_SELECTOR, '.note-item[data-slug="baltimorean"]'
        ))
        # Click-lock the item, then click the ? box.
        item.click()
        image_box = item.find_element(By.CSS_SELECTOR, ".note-item__image-box")
        image_box.click()
        modal = self.wait_for(lambda: item.find_element(
            By.CSS_SELECTOR, ".note-palette-modal"
        ))
        # Both swatches present, each w. its human-readable label.
        labels = [
            el.text.strip()
            for el in modal.find_elements(By.CSS_SELECTOR, ".note-swatch-label")
        ]
        self.assertIn("Baltimore", labels)
        self.assertIn("Maryland", labels)
        # Bardo + Sheol (stargazer's palettes) must NOT bleed through.
        self.assertNotIn("Bardo", labels)
        self.assertNotIn("Sheol", labels)

    # ── T6 — Baltimore swatch commit + dashboard reflects unlock + ts

    def test_palette_baltimore_commit_persists_and_unlocks_dashboard_swatch(self):
        """Clicking the Baltimore swatch body previews palette-baltimore on
        the body; OK commits — Note.palette + user.palette both saved — so
        navigating to /dashboard/ shows palette-baltimore active + the
        Palette applet swatch unlocked with Baltimorean shoptalk +
        the unlocked-date timestamp wired through."""
        note = Note.objects.create(
            user=self.gamer, slug="baltimorean", earned_at=timezone.now(),
        )
        self.create_pre_authenticated_session("ard@bawlmore.io")
        self.browser.get(self.my_notes_url)

        item = self.wait_for(lambda: self.browser.find_element(
            By.CSS_SELECTOR, '.note-item[data-slug="baltimorean"]'
        ))
        item.click()
        item.find_element(By.CSS_SELECTOR, ".note-item__image-box").click()
        modal = self.wait_for(lambda: item.find_element(
            By.CSS_SELECTOR, ".note-palette-modal"
        ))
        baltimore_body = modal.find_element(
            By.CSS_SELECTOR, ".palette-baltimore .note-swatch-body"
        )
        self.browser.execute_script(
            "arguments[0].dispatchEvent(new MouseEvent('click', {bubbles: true}))",
            baltimore_body,
        )
        # Body previews the palette
        self.wait_for(lambda: self.assertIn(
            "palette-baltimore",
            self.browser.find_element(By.TAG_NAME, "body").get_attribute("class"),
        ))
        # OK commits
        confirm = self.wait_for(lambda: item.find_element(
            By.CSS_SELECTOR, ".note-palette-confirm"
        ))
        confirm.find_element(By.CSS_SELECTOR, ".btn.btn-confirm").click()
        self.wait_for(lambda: self.assertFalse(
            item.find_elements(By.CSS_SELECTOR, ".note-palette-modal")
        ))

        # Navigate to dashboard — palette-baltimore persists on body +
        # the Palette applet swatch is unlocked w. the "recognized via
        # Baltimorean" data-description + a non-empty unlocked-date.
        self.browser.get(self.live_server_url)
        self.wait_for(lambda: self.assertIn(
            "palette-baltimore",
            self.browser.find_element(By.TAG_NAME, "body").get_attribute("class"),
        ))
        palette_applet = self.wait_for(lambda: self.browser.find_element(
            By.ID, "id_applet_palette"
        ))
        baltimore = palette_applet.find_element(
            By.CSS_SELECTOR, ".swatch.palette-baltimore"
        )
        self.assertNotIn("locked", baltimore.get_attribute("class"))
        self.assertIn("Baltimorean", baltimore.get_attribute("data-description"))
        # Unlocked-date carries the Note's earned_at as ISO — non-empty proves
        # the wire from Note.earned_at through _palettes_for_user.
        self.assertTrue(baltimore.get_attribute("data-unlocked-date"))
        # And the same Note row in DB now carries the palette commit.
        note.refresh_from_db()
        self.assertEqual(note.palette, "palette-baltimore")
