"""Functional tests for the Note system.

Note is Earthman's achievement analogue — account-level unlocks earned when
the socius observes a qualifying action.  These tests cover the Stargazer Note:
earned on the first valid personal sky save (dashboard My Sky applet or sky page),
outside of any game room.

Two test classes — one per surface that can trigger the unlock — each asserting both
the negative (disabled/incomplete save does nothing) and positive (first valid save
fires the banner) conditions.

T2 (Dashboard full flow) is split across three focused tests:
  T2a — save → banner → FYI → recognition page item
  T2b — palette modal flow on recognition page
  T2c — dashboard palette applet reflects Note palette unlock
"""
import json as _json

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


# Shared natal chart fixture — same birth data as test_applet_my_sky.py.
_CHART_FIXTURE = {
    "planets": {
        "Sun":     {"sign": "Gemini",      "degree":  66.7, "retrograde": False},
        "Moon":    {"sign": "Taurus",      "degree":  43.0, "retrograde": False},
        "Mercury": {"sign": "Taurus",      "degree":  55.0, "retrograde": False},
        "Venus":   {"sign": "Gemini",      "degree":  63.3, "retrograde": False},
        "Mars":    {"sign": "Leo",         "degree": 132.0, "retrograde": False},
        "Jupiter": {"sign": "Capricorn",   "degree": 292.0, "retrograde": True},
        "Saturn":  {"sign": "Virgo",       "degree": 153.0, "retrograde": False},
        "Uranus":  {"sign": "Pisces",      "degree": 322.0, "retrograde": False},
        "Neptune": {"sign": "Aquarius",    "degree": 323.0, "retrograde": True},
        "Pluto":   {"sign": "Sagittarius", "degree": 269.0, "retrograde": True},
    },
    "houses": {
        "cusps": [180, 210, 240, 270, 300, 330, 0, 30, 60, 90, 120, 150],
        "asc": 180.0, "mc": 90.0,
    },
    "elements": {"Fire": 1, "Water": 0, "Stone": 2, "Air": 4, "Time": 1, "Space": 1},
    "aspects": [],
    "distinctions": {
        "1": 0, "2": 0, "3": 2, "4": 0, "5": 0, "6": 0,
        "7": 1, "8": 0, "9": 2, "10": 1, "11": 1, "12": 2,
    },
    "house_system": "O",
    "timezone": "Europe/London",
}


def _mock_preview_js(fixture):
    """Intercepts /sky/preview with a fixture; lets /sky/save reach the real server."""
    return f"""
        const FIXTURE = {_json.dumps(fixture)};
        window._origFetch = window.fetch;
        window.fetch = function(url, opts) {{
            if (url.includes('/sky/preview')) {{
                return Promise.resolve({{
                    ok: true,
                    json: () => Promise.resolve(FIXTURE),
                }});
            }}
            return window._origFetch(url, opts);
        }};
    """


def _fill_valid_sky_form(browser):
    """Populate form fields and fire input events to trigger schedulePreview."""
    browser.execute_script("""
        document.getElementById('id_nf_date').value = '1990-06-15';
        document.getElementById('id_nf_lat').value  = '51.5074';
        document.getElementById('id_nf_lon').value  = '-0.1278';
        document.getElementById('id_nf_tz').value   = 'Europe/London';
        document.getElementById('id_nf_date').dispatchEvent(
            new Event('input', {bubbles: true})
        );
    """)


# ──────────────────────────────────────────────────────────────────────────────
# Surface A — My Sky applet on the Dashboard
# ──────────────────────────────────────────────────────────────────────────────

class StargazerNoteFromDashboardTest(FunctionalTest):
    """Stargazer Note triggered from the My Sky applet."""

    def setUp(self):
        super().setUp()
        self.browser.set_window_size(800, 1200)
        Applet.objects.get_or_create(
            slug="my-sky",
            defaults={"name": "My Sky", "grid_cols": 6, "grid_rows": 6, "context": "dashboard"},
        )
        Applet.objects.get_or_create(
            slug="palette",
            defaults={"name": "Palettes", "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"},
        )
        self.gamer = User.objects.create(email="stargazer@test.io")

    # ── T1 ───────────────────────────────────────────────────────────────────

    def test_disabled_save_button_does_not_unlock_note(self):
        """SAVE SKY is disabled before a valid preview fires.
        No Note banner appears while the button remains disabled."""
        self.create_pre_authenticated_session("stargazer@test.io")
        self.browser.get(self.live_server_url)

        confirm_btn = self.wait_for(
            lambda: self.browser.find_element(By.ID, "id_sky_confirm")
        )
        self.assertIsNotNone(confirm_btn.get_attribute("disabled"))
        self.assertFalse(self.browser.find_elements(By.CSS_SELECTOR, ".note-banner"))

    # ── T2a ──────────────────────────────────────────────────────────────────

    def test_first_valid_save_from_applet_fires_banner_and_leads_to_note_page(self):
        """First valid SAVE SKY from the My Sky applet fires the Stargazer banner.
        FYI button navigates to /billboard/my-notes/ showing the Stargazer item."""
        self.create_pre_authenticated_session("stargazer@test.io")
        self.browser.get(self.live_server_url)

        self.wait_for(lambda: self.browser.find_element(By.ID, "id_applet_sky_form_wrap"))
        self.browser.execute_script(_mock_preview_js(_CHART_FIXTURE))
        _fill_valid_sky_form(self.browser)

        confirm_btn = self.browser.find_element(By.ID, "id_sky_confirm")
        self.wait_for(lambda: self.assertIsNone(confirm_btn.get_attribute("disabled")))
        confirm_btn.click()

        # Banner slides in below the Dash h2
        banner = self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, ".note-banner")
        )
        self.assertIn(
            "Stargazer",
            banner.find_element(By.CSS_SELECTOR, ".note-banner__title").text,
        )
        banner.find_element(By.CSS_SELECTOR, ".note-banner__description")
        banner.find_element(By.CSS_SELECTOR, ".note-banner__timestamp")
        # Per the Brief sprint, .note-banner__image is now a clickable <a> for
        # NOTE_UNLOCK kind — square goes to my_notes.html (the legacy FYI
        # behavior preserved on the square).
        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")
        banner.find_element(By.CSS_SELECTOR, ".btn.btn-cancel")    # NVM
        fyi = banner.find_element(By.CSS_SELECTOR, ".btn.btn-info")  # FYI

        # FYI now navigates to the underlying Brief's Post detail
        # (/billboard/post/<uuid>/) — the GET render is the mark-read contract.
        fyi.click()
        self.wait_for(
            lambda: self.assertRegex(self.browser.current_url, r"/billboard/post/[0-9a-f-]+/")
        )

        # Square (.note-banner__image) preserves the jump-direct-to-my-notes
        # behavior. Reload the dashboard and re-fire the banner via a stash.
        self.browser.get(self.live_server_url + "/billboard/my-notes/")

        # Note page: one Stargazer item
        item = self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, ".note-list .note-item")
        )
        self.assertIn(
            "Stargazer",
            item.find_element(By.CSS_SELECTOR, ".note-item__title").text,
        )
        item.find_element(By.CSS_SELECTOR, ".note-item__description")
        item.find_element(By.CSS_SELECTOR, ".note-item__image-box")

    # ── T2b ──────────────────────────────────────────────────────────────────

    def _open_modal_and_click_bardo(self):
        """Helper: navigate to /billboard/my-notes/, open modal, click bardo swatch body.
        Returns (modal, confirm_menu) after the confirm bar is visible."""
        self.browser.get(self.live_server_url + "/billboard/my-notes/")
        image_box = self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, ".note-item__image-box")
        )
        image_box.click()
        modal = self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, ".note-palette-modal")
        )
        bardo_body = modal.find_element(By.CSS_SELECTOR, ".palette-bardo .note-swatch-body")
        self.browser.execute_script(
            "arguments[0].dispatchEvent(new MouseEvent('click', {bubbles: true}))",
            bardo_body,
        )
        confirm_menu = self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, ".note-palette-confirm")
        )
        return modal, confirm_menu

    def test_note_page_swatch_previews_palette_sitewide_and_ok_persists(self):
        """Clicking a swatch previews that palette on the whole body.
        OK commits it — Note.palette and user.palette both saved — so it
        survives navigation to a new page."""
        Note.objects.create(
            user=self.gamer, slug="stargazer", earned_at=timezone.now(),
        )
        self.create_pre_authenticated_session("stargazer@test.io")

        modal, confirm = self._open_modal_and_click_bardo()

        # Swatch click previews bardo on the whole body
        self.wait_for(
            lambda: self.assertIn(
                "palette-bardo",
                self.browser.find_element(By.TAG_NAME, "body").get_attribute("class"),
            )
        )
        # Modal still open
        self.assertTrue(self.browser.find_elements(By.CSS_SELECTOR, ".note-palette-modal"))

        # OK → modal closes, ? box replaced by bardo swatch
        confirm.find_element(By.CSS_SELECTOR, ".btn.btn-confirm").click()
        self.wait_for(lambda: self.assertFalse(
            self.browser.find_elements(By.CSS_SELECTOR, ".note-palette-modal")
        ))
        item = self.browser.find_element(By.CSS_SELECTOR, ".note-item")
        self.assertTrue(item.find_elements(By.CSS_SELECTOR, ".note-item__palette.palette-bardo"))
        self.assertFalse(item.find_elements(By.CSS_SELECTOR, ".note-item__image-box"))

        # Navigate away — palette persists (user.palette was saved)
        self.browser.get(self.live_server_url)
        self.wait_for(
            lambda: self.assertIn(
                "palette-bardo",
                self.browser.find_element(By.TAG_NAME, "body").get_attribute("class"),
            )
        )

    def test_note_swatch_nvm_reverts_body_palette(self):
        """NVM in the confirm bar reverts the sitewide body palette back to
        what it was before the swatch was clicked."""
        Note.objects.create(
            user=self.gamer, slug="stargazer", earned_at=timezone.now(),
        )
        self.create_pre_authenticated_session("stargazer@test.io")

        # Record the original palette before opening the modal
        self.browser.get(self.live_server_url + "/billboard/my-notes/")
        original_classes = self.browser.find_element(
            By.TAG_NAME, "body"
        ).get_attribute("class")
        original_palette = next(
            (c for c in original_classes.split() if c.startswith("palette-")), None
        )

        modal, confirm = self._open_modal_and_click_bardo()

        # Bardo is previewed
        self.wait_for(
            lambda: self.assertIn(
                "palette-bardo",
                self.browser.find_element(By.TAG_NAME, "body").get_attribute("class"),
            )
        )

        # NVM reverts
        confirm.find_element(By.CSS_SELECTOR, ".btn.btn-cancel").click()
        self.wait_for(
            lambda: self.assertNotIn(
                "palette-bardo",
                self.browser.find_element(By.TAG_NAME, "body").get_attribute("class"),
            )
        )
        if original_palette:
            self.assertIn(
                original_palette,
                self.browser.find_element(By.TAG_NAME, "body").get_attribute("class"),
            )
        # Swatch modal remains open after NVM; only the confirm bar is hidden
        self.assertTrue(
            self.browser.find_elements(By.CSS_SELECTOR, ".note-palette-modal")
        )
        confirm_el = self.browser.find_element(By.CSS_SELECTOR, ".note-palette-confirm")
        self.assertFalse(confirm_el.is_displayed())

    # ── T2c ──────────────────────────────────────────────────────────────────

    def test_dashboard_palette_applet_reflects_note_palette_unlock(self):
        """After palette unlock via Note, the Dashboard Palette applet shows
        the palette swatch as unlocked with Stargazer shoptalk."""
        Note.objects.create(
            user=self.gamer, slug="stargazer", earned_at=timezone.now(),
            palette="palette-bardo",
        )
        self.create_pre_authenticated_session("stargazer@test.io")
        self.browser.get(self.live_server_url)

        palette_applet = self.wait_for(
            lambda: self.browser.find_element(By.ID, "id_applet_palette")
        )
        bardo = palette_applet.find_element(By.CSS_SELECTOR, ".swatch.palette-bardo")
        self.assertNotIn("locked", bardo.get_attribute("class"))
        bardo_ok = bardo.find_element(By.CSS_SELECTOR, ".palette-ok")
        self.assertIn("btn-confirm", bardo_ok.get_attribute("class"))
        self.assertNotIn("btn-disabled", bardo_ok.get_attribute("class"))
        self.assertIn("Stargazer", bardo.get_attribute("data-description"))


# ──────────────────────────────────────────────────────────────────────────────
# Surface B — /dashboard/sky/ standalone page
# ──────────────────────────────────────────────────────────────────────────────

class StargazerNoteFromSkyPageTest(FunctionalTest):
    """Stargazer Note triggered from the standalone sky page.

    T3 — disabled save on sky page does not fire Note.
    T4 — first valid save fires banner; NVM dismisses it.
    T5 — already-earned Note does not re-show banner on subsequent save.
    """

    def setUp(self):
        super().setUp()
        self.browser.set_window_size(800, 1200)
        Applet.objects.get_or_create(
            slug="my-sky",
            defaults={"name": "My Sky", "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"},
        )
        self.gamer = User.objects.create(email="stargazer@test.io")
        self.sky_url = self.live_server_url + "/dashboard/sky/"

    # ── T3 ───────────────────────────────────────────────────────────────────

    def test_disabled_save_on_sky_page_does_not_unlock_note(self):
        """On /dashboard/sky/, SAVE SKY is disabled until preview fires.
        No Note banner appears while the button is disabled."""
        self.create_pre_authenticated_session("stargazer@test.io")
        self.browser.get(self.sky_url)

        confirm_btn = self.wait_for(
            lambda: self.browser.find_element(By.ID, "id_sky_confirm")
        )
        self.assertIsNotNone(confirm_btn.get_attribute("disabled"))
        self.assertFalse(self.browser.find_elements(By.CSS_SELECTOR, ".note-banner"))

    # ── T4 ───────────────────────────────────────────────────────────────────

    def test_first_valid_save_on_sky_page_fires_banner_and_nvm_dismisses(self):
        """First valid SAVE SKY on /dashboard/sky/ fires the Stargazer banner.
        NVM (.btn.btn-cancel) dismisses it."""
        self.create_pre_authenticated_session("stargazer@test.io")
        self.browser.get(self.sky_url)

        self.wait_for(lambda: self.browser.find_element(By.ID, "id_sky_confirm"))
        self.browser.execute_script(_mock_preview_js(_CHART_FIXTURE))
        _fill_valid_sky_form(self.browser)

        confirm_btn = self.browser.find_element(By.ID, "id_sky_confirm")
        self.wait_for(lambda: self.assertIsNone(confirm_btn.get_attribute("disabled")))
        confirm_btn.click()

        banner = self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, ".note-banner")
        )
        self.assertIn(
            "Stargazer",
            banner.find_element(By.CSS_SELECTOR, ".note-banner__title").text,
        )

        banner.find_element(By.CSS_SELECTOR, ".btn.btn-cancel").click()
        self.wait_for(lambda: self.assertFalse(
            self.browser.find_elements(By.CSS_SELECTOR, ".note-banner")
        ))

    # ── T5 ───────────────────────────────────────────────────────────────────

    def test_already_earned_note_does_not_show_banner_on_subsequent_save(self):
        """When Stargazer is already in the database for this user, a valid sky
        save does not fire another Note banner."""
        Note.objects.create(
            user=self.gamer,
            slug="stargazer",
            earned_at=timezone.now(),
        )

        self.create_pre_authenticated_session("stargazer@test.io")
        self.browser.get(self.sky_url)

        self.wait_for(lambda: self.browser.find_element(By.ID, "id_sky_confirm"))
        self.browser.execute_script(_mock_preview_js(_CHART_FIXTURE))
        _fill_valid_sky_form(self.browser)

        confirm_btn = self.browser.find_element(By.ID, "id_sky_confirm")
        self.wait_for(lambda: self.assertIsNone(confirm_btn.get_attribute("disabled")))
        confirm_btn.click()

        # Wait for save to complete (wheel renders) then assert no banner
        self.wait_for(lambda: self.assertTrue(
            self.browser.find_elements(By.CSS_SELECTOR, ".nw-root")
        ))
        self.assertFalse(self.browser.find_elements(By.CSS_SELECTOR, ".note-banner"))


# ──────────────────────────────────────────────────────────────────────────────
# Title equip — DON/DOFF buttons on the note item
# ──────────────────────────────────────────────────────────────────────────────

@tag("sequential")
class NoteEquipTitleTest(FunctionalTest):
    """DON button equips the note title as the sitewide greeting; DOFF restores it."""

    def setUp(self):
        super().setUp()
        self.browser.set_window_size(800, 1200)
        Applet.objects.get_or_create(
            slug="notes",
            defaults={"name": "My Notes", "grid_cols": 4, "grid_rows": 4, "context": "billboard"},
        )
        self.gamer = User.objects.create(email="stargazer@test.io")

    def test_don_equips_title_greeting_and_doff_restores(self):
        """DON replaces 'Earthman' with the note title; DOFF restores 'Earthman'.
        DON/DOFF follow the game-kit equip button pattern: the active btn is normal,
        the inactive btn is × + btn-disabled."""
        Note.objects.create(
            user=self.gamer, slug="stargazer", earned_at=timezone.now(),
        )
        self.create_pre_authenticated_session("stargazer@test.io")
        self.browser.get(self.live_server_url + "/billboard/my-notes/")

        note_item = self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, ".note-item")
        )
        # Click the note to lock it — makes DON/DOFF opacity:1 and interactable
        note_item.click()
        don_btn  = note_item.find_element(By.CSS_SELECTOR, ".btn-equip")
        doff_btn = note_item.find_element(By.CSS_SELECTOR, ".btn-unequip")

        # Initial state: DON active ("DON"), DOFF disabled ("×")
        self.assertNotIn("btn-disabled", don_btn.get_attribute("class"))
        self.assertEqual(don_btn.text, "DON")
        self.assertIn("btn-disabled", doff_btn.get_attribute("class"))
        self.assertEqual(doff_btn.text, "×")

        # Click DON → greeting changes to the note title
        don_btn.click()
        self.wait_for(
            lambda: self.assertIn(
                "Stargazer",
                self.browser.find_element(By.ID, "id_greeting_name").text,
            )
        )
        self.assertIn("btn-disabled", don_btn.get_attribute("class"))
        self.assertEqual(don_btn.text, "×")
        self.assertNotIn("btn-disabled", doff_btn.get_attribute("class"))
        self.assertEqual(doff_btn.text, "DOFF")

        # Click DOFF → greeting restored to "Earthman"
        doff_btn.click()
        self.wait_for(
            lambda: self.assertIn(
                "Earthman",
                self.browser.find_element(By.ID, "id_greeting_name").text,
            )
        )
        self.assertNotIn("btn-disabled", don_btn.get_attribute("class"))
        self.assertEqual(don_btn.text, "DON")
        self.assertIn("btn-disabled", doff_btn.get_attribute("class"))
        self.assertEqual(doff_btn.text, "×")
