"""Functional tests for the My Sky dashboard feature.

My Sky is a dashboard applet linking to /dashboard/sky/ — a full-page
sky (natal chart) interface where the user can save their personal sky
to their account (stored on the User model, independent of any game room).
"""

import json as _json
import unittest
import zoneinfo
from datetime import datetime

from selenium.webdriver.common.by import By

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

from .base import FunctionalTest


# Chart fixture — May 27 2008, 12:12 PM, Morganza MD (38.3754°N, 76.6955°W).
# Sun (6.7° Gemini) and Venus (3.3° Gemini) are 3.4° apart — a clear conjunction.
_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": "America/New_York",
}


class MySkyAppletTest(FunctionalTest):
    """My Sky applet appears on the dashboard and links to the sky page."""

    def setUp(self):
        super().setUp()
        Applet.objects.get_or_create(
            slug="my-sky",
            defaults={"name": "My Sky", "grid_cols": 6, "grid_rows": 6, "context": "dashboard"},
        )
        self.gamer = User.objects.create(email="stargazer@test.io")

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

    def test_my_sky_applet_links_to_sky_page_with_form(self):
        """Applet is visible on dashboard; link leads to /dashboard/sky/ with
        all sky form fields present."""
        self.create_pre_authenticated_session("stargazer@test.io")
        self.browser.get(self.live_server_url)

        # 1. Applet is on the dashboard
        applet = self.wait_for(
            lambda: self.browser.find_element(By.ID, "id_applet_my_sky")
        )

        # 2. Heading contains a link whose text is "My Sky"
        link = applet.find_element(By.CSS_SELECTOR, "h2 a")
        self.assertIn("MY SKY", link.text.upper())

        # 3. Clicking the link navigates to /dashboard/sky/
        link.click()
        self.wait_for(
            lambda: self.assertRegex(self.browser.current_url, r"/dashboard/sky/$")
        )

        # 4. All sky form fields are present
        self.browser.find_element(By.ID, "id_nf_date")
        self.browser.find_element(By.ID, "id_nf_time")
        self.browser.find_element(By.ID, "id_nf_place")
        self.browser.find_element(By.ID, "id_nf_lat")
        self.browser.find_element(By.ID, "id_nf_lon")
        self.browser.find_element(By.ID, "id_nf_tz")
        self.browser.find_element(By.ID, "id_sky_confirm")


class MySkyLocalStorageTest(FunctionalTest):
    """My Sky form fields persist to localStorage across visits."""

    def setUp(self):
        super().setUp()
        Applet.objects.get_or_create(
            slug="my-sky",
            defaults={"name": "My Sky", "grid_cols": 6, "grid_rows": 6, "context": "dashboard"},
        )
        self.gamer = User.objects.create(email="stargazer@test.io")
        self.sky_url = self.live_server_url + "/dashboard/sky/"

    def _fill_form(self):
        """Set date, lat, lon directly — bypasses Nominatim network call."""
        self.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_place').value = 'London, UK';"
            "document.getElementById('id_nf_tz').value    = 'Europe/London';"
        )
        # Fire input events so the localStorage save listener triggers
        self.browser.execute_script("""
            ['id_nf_date','id_nf_lat','id_nf_lon','id_nf_place','id_nf_tz'].forEach(id => {
                document.getElementById(id)
                    .dispatchEvent(new Event('input', {bubbles: true}));
            });
        """)

    def _field_values(self):
        return self.browser.execute_script("""
            return {
                date:  document.getElementById('id_nf_date').value,
                lat:   document.getElementById('id_nf_lat').value,
                lon:   document.getElementById('id_nf_lon').value,
                place: document.getElementById('id_nf_place').value,
                tz:    document.getElementById('id_nf_tz').value,
            };
        """)

    # ── T2 ───────────────────────────────────────────────────────────────────

    @unittest.skip('localstorage saving temporarily removed')
    def test_sky_form_fields_repopulated_after_page_refresh(self):
        """Form values survive a full page refresh via localStorage."""
        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_nf_date"))
        self._fill_form()

        self.browser.refresh()

        self.wait_for(lambda: self.browser.find_element(By.ID, "id_nf_date"))
        values = self._field_values()
        self.assertEqual(values["date"],  "1990-06-15")
        self.assertEqual(values["lat"],   "51.5074")
        self.assertEqual(values["lon"],   "-0.1278")
        self.assertEqual(values["place"], "London, UK")
        self.assertEqual(values["tz"],    "Europe/London")


class MySkyAppletWheelTest(FunctionalTest):
    """Saved natal chart renders as an interactive wheel inside the My Sky applet."""

    def setUp(self):
        super().setUp()
        Applet.objects.get_or_create(
            slug="my-sky",
            defaults={"name": "My Sky", "grid_cols": 6, "grid_rows": 6, "context": "dashboard"},
        )
        self.gamer = User.objects.create(email="stargazer@test.io")
        self.gamer.sky_chart_data  = _CHART_FIXTURE
        self.gamer.sky_birth_place = "Baltimore, MD, US"
        self.gamer.save()

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

    def test_saved_sky_wheel_renders_with_element_tooltip_in_applet(self):
        """When the user has saved sky data, the natal wheel appears in the My Sky
        applet and clicking an element-ring slice shows the tooltip.
        (Planet click tooltip is covered by SkyWheelSpec.js T3/T4/T5.)"""
        self.create_pre_authenticated_session("stargazer@test.io")
        self.browser.get(self.live_server_url)

        # 1. Wheel SVG is drawn inside the applet
        self.wait_for(lambda: self.assertTrue(
            self.browser.find_element(
                By.CSS_SELECTOR, "#id_applet_my_sky .nw-root"
            )
        ))

        # 2. Clicking an element-ring slice shows the tooltip (JS click bypasses
        #    scroll-into-view restriction inside the overflow-masked applet).
        slice_el = self.browser.find_element(
            By.CSS_SELECTOR, "#id_applet_my_sky .nw-element-group"
        )
        self.browser.execute_script(
            "arguments[0].dispatchEvent(new MouseEvent('click', {bubbles: true}));",
            slice_el,
        )
        self.wait_for(lambda: self.assertEqual(
            self.browser.find_element(By.ID, "id_sky_tooltip")
                .value_of_css_property("display"),
            "block",
        ))


class MySkyAppletFormTest(FunctionalTest):
    """My Sky applet shows sky entry form when no sky data is saved."""

    def setUp(self):
        super().setUp()
        Applet.objects.get_or_create(
            slug="my-sky",
            defaults={"name": "My Sky", "grid_cols": 6, "grid_rows": 6, "context": "dashboard"},
        )
        self.gamer = User.objects.create(email="stargazer@test.io")

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

    def test_applet_shows_entry_form_when_no_sky_saved(self):
        """When no sky data is saved the My Sky applet shows all sky form
        fields and a disabled SAVE SKY button; no wheel is drawn yet."""
        self.create_pre_authenticated_session("stargazer@test.io")
        self.browser.get(self.live_server_url)

        applet = self.wait_for(
            lambda: self.browser.find_element(By.ID, "id_applet_my_sky")
        )

        applet.find_element(By.ID, "id_nf_date")
        applet.find_element(By.ID, "id_nf_time")
        applet.find_element(By.ID, "id_nf_place")
        applet.find_element(By.ID, "id_nf_lat")
        applet.find_element(By.ID, "id_nf_lon")
        applet.find_element(By.ID, "id_nf_tz")
        applet.find_element(By.ID, "id_sky_confirm")

        self.assertFalse(applet.find_elements(By.CSS_SELECTOR, ".nw-root"))

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

    def test_applet_form_disappears_and_wheel_draws_after_save(self):
        """Filling the applet form and clicking SAVE SKY hides the form wrap
        and draws the natal wheel in its place."""
        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")
        )

        # Mock fetch: preview → chart fixture; save → {saved: true}
        self.browser.execute_script("""
            const FIXTURE = """ + _json.dumps(_CHART_FIXTURE) + """;
            window._origFetch = window.fetch;
            window.fetch = function(url, opts) {
                if (url.includes('/sky/preview')) {
                    return Promise.resolve({
                        ok: true,
                        json: () => Promise.resolve(FIXTURE),
                    });
                }
                if (url.includes('/sky/save')) {
                    return Promise.resolve({
                        ok: true,
                        json: () => Promise.resolve({saved: true}),
                    });
                }
                return window._origFetch(url, opts);
            };
        """)

        # Fill required fields and fire input to trigger schedulePreview
        self.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})
            );
        """)

        # Wait for confirm button to be enabled (preview resolved)
        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()

        # Form wrap should become hidden
        form_wrap = self.browser.find_element(By.ID, "id_applet_sky_form_wrap")
        self.wait_for(lambda: self.assertEqual(
            form_wrap.value_of_css_property("display"), "none"
        ))

        # Natal wheel should be drawn inside the applet
        self.wait_for(lambda: self.assertTrue(
            self.browser.find_element(
                By.CSS_SELECTOR, "#id_applet_my_sky .nw-root"
            )
        ))


class MySkyTimezoneRefreshTest(FunctionalTest):
    """Selecting a new birth place must refresh the auto-detected timezone."""

    def setUp(self):
        super().setUp()
        Applet.objects.get_or_create(
            slug="my-sky",
            defaults={"name": "My Sky", "grid_cols": 6, "grid_rows": 6, "context": "dashboard"},
        )
        self.gamer = User.objects.create(email="stargazer@test.io")
        self.gamer.sky_chart_data  = _CHART_FIXTURE
        self.gamer.sky_birth_place = "Baltimore, MD, US"
        self.gamer.sky_birth_tz    = "America/New_York"
        self.gamer.sky_birth_lat   = 39.2904
        self.gamer.sky_birth_lon   = -76.6122
        self.gamer.sky_birth_dt    = datetime(
            1990, 6, 15, 12, 0, tzinfo=zoneinfo.ZoneInfo("UTC")
        )
        self.gamer.save()
        self.sky_url = self.live_server_url + "/dashboard/sky/"

    def test_changing_place_refreshes_auto_detected_timezone(self):
        """Picking a new place via the suggestion dropdown must update the
        timezone field to the new place's auto-detected TZ rather than sticking
        on the previously saved value."""
        self.create_pre_authenticated_session("stargazer@test.io")
        self.browser.get(self.sky_url)

        # 1. Initial state: TZ field shows the saved America/New_York
        tz_input = self.wait_for(lambda: self.browser.find_element(By.ID, "id_nf_tz"))
        self.assertEqual(tz_input.get_attribute("value"), "America/New_York")

        # 2. Mock Nominatim search + sky/preview to return Camarillo / Pacific TZ
        camarillo_chart = dict(_CHART_FIXTURE)
        camarillo_chart["timezone"] = "America/Los_Angeles"
        self.browser.execute_script("""
            const NEW_CHART = """ + _json.dumps(camarillo_chart) + """;
            window._origFetch = window.fetch;
            window.fetch = function(url, opts) {
                if (typeof url === 'string' && url.includes('nominatim.openstreetmap.org/search')) {
                    return Promise.resolve({
                        ok: true,
                        json: () => Promise.resolve([{
                            display_name: 'Camarillo, Ventura County, California, US',
                            lat: '34.2176', lon: '-119.0384',
                        }]),
                    });
                }
                if (typeof url === 'string' && url.includes('/sky/preview')) {
                    return Promise.resolve({
                        ok: true,
                        json: () => Promise.resolve(NEW_CHART),
                    });
                }
                return window._origFetch(url, opts);
            };
        """)

        # 3. Type into the place input → click first Nominatim suggestion
        place_input = self.browser.find_element(By.ID, "id_nf_place")
        place_input.clear()
        place_input.send_keys("Cam")
        suggestion = self.wait_for_slow(lambda: self.browser.find_element(
            By.CSS_SELECTOR, ".sky-suggestion-item"
        ))
        suggestion.click()

        # 4. After preview resolves, TZ field should show the new auto-detected TZ
        self.wait_for(lambda: self.assertEqual(
            tz_input.get_attribute("value"), "America/Los_Angeles"
        ))


class MySkyApertureSnapScrollTest(FunctionalTest):
    """Once sky data is saved, the .sky-page aperture is a snap-binary scroller
    (wheel section + form section, each filling the aperture). Clicking SAVE
    SKY animates the aperture back to the top (the wheel)."""

    def setUp(self):
        super().setUp()
        Applet.objects.get_or_create(
            slug="my-sky",
            defaults={"name": "My Sky", "grid_cols": 6, "grid_rows": 6, "context": "dashboard"},
        )
        self.gamer = User.objects.create(email="stargazer@test.io")
        self.gamer.sky_chart_data  = _CHART_FIXTURE
        self.gamer.sky_birth_place = "Baltimore, MD, US"
        self.gamer.sky_birth_tz    = "America/New_York"
        self.gamer.sky_birth_lat   = 39.2904
        self.gamer.sky_birth_lon   = -76.6122
        self.gamer.sky_birth_dt    = datetime(
            1990, 6, 15, 12, 0, tzinfo=zoneinfo.ZoneInfo("UTC")
        )
        self.gamer.save()
        self.sky_url = self.live_server_url + "/dashboard/sky/"

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

    def test_aperture_has_snap_y_mandatory_when_sky_saved(self):
        """When sky is saved, the .sky-page aperture has scroll-snap-type:y mandatory
        and both wheel-col & form-col have scroll-snap-align:start. Without these
        the layout is a free scroll instead of the binary wheel<->form toggle."""
        self.browser.set_window_size(820, 520)
        self.create_pre_authenticated_session("stargazer@test.io")
        self.browser.get(self.sky_url)
        self.wait_for(lambda: self.browser.find_element(By.CLASS_NAME, "sky-page"))

        styles = self.browser.execute_script("""
            const ap = document.querySelector('.sky-page');
            const wheel = document.querySelector('.sky-page .sky-wheel-col');
            const form  = document.querySelector('.sky-page .sky-form-col');
            return {
                snapType:   getComputedStyle(ap).scrollSnapType,
                wheelAlign: getComputedStyle(wheel).scrollSnapAlign,
                formAlign:  getComputedStyle(form).scrollSnapAlign,
            };
        """)
        self.assertIn("y", styles["snapType"])
        self.assertIn("mandatory", styles["snapType"])
        self.assertEqual(styles["wheelAlign"], "start")
        self.assertEqual(styles["formAlign"], "start")

    # ── T2 ───────────────────────────────────────────────────────────────────

    def test_save_sky_scrolls_aperture_back_to_top(self):
        """Clicking SAVE SKY from the form section animates the aperture's
        scrollTop back to 0 (the wheel)."""
        self.browser.set_window_size(820, 520)
        self.create_pre_authenticated_session("stargazer@test.io")
        self.browser.get(self.sky_url)
        self.wait_for(lambda: self.browser.find_element(By.CLASS_NAME, "sky-page"))

        # Mock /sky/save so the click resolves without real server work
        self.browser.execute_script("""
            window._origFetch = window.fetch;
            window.fetch = function(url, opts) {
                if (typeof url === 'string' && url.includes('/sky/save')) {
                    return Promise.resolve({
                        ok: true,
                        json: () => Promise.resolve({saved: true}),
                    });
                }
                return window._origFetch(url, opts);
            };
        """)

        # Scroll the aperture down to the form section
        self.browser.execute_script(
            "document.querySelector('.sky-page').scrollTop = 9999;"
        )
        self.wait_for(lambda: self.assertGreater(
            self.browser.execute_script(
                "return document.querySelector('.sky-page').scrollTop;"
            ),
            10,
        ))

        self.browser.find_element(By.ID, "id_sky_confirm").click()

        # After save resolves, aperture scrollTop animates back to 0
        self.wait_for(lambda: self.assertEqual(
            self.browser.execute_script(
                "return Math.round(document.querySelector('.sky-page').scrollTop);"
            ),
            0,
        ))


class MySkyAppletDelTest(FunctionalTest):
    """My Sky applet (on /dashboard/) gets a parity DEL btn at the wheel
    center: clicking opens the global guard portal; OK clears the saved sky
    on the User model & the relevant localStorage entry, then the applet
    swaps back to its form rendering on reload."""

    def setUp(self):
        super().setUp()
        Applet.objects.get_or_create(
            slug="my-sky",
            defaults={"name": "My Sky", "grid_cols": 6, "grid_rows": 6, "context": "dashboard"},
        )
        self.gamer = User.objects.create(email="stargazer@test.io")
        self.gamer.sky_chart_data  = _CHART_FIXTURE
        self.gamer.sky_birth_place = "Baltimore, MD, US"
        self.gamer.sky_birth_tz    = "America/New_York"
        self.gamer.sky_birth_lat   = 39.2904
        self.gamer.sky_birth_lon   = -76.6122
        self.gamer.sky_birth_dt    = datetime(
            1990, 6, 15, 12, 0, tzinfo=zoneinfo.ZoneInfo("UTC")
        )
        self.gamer.save()
        self.dash_url = self.live_server_url + "/"

    def test_applet_del_clears_user_sky_and_swaps_to_form(self):
        self.create_pre_authenticated_session("stargazer@test.io")
        self.browser.get(self.dash_url)

        # 1. Applet shows wheel + DEL btn
        del_btn = self.wait_for(lambda: self.browser.find_element(
            By.CSS_SELECTOR, "#id_applet_my_sky .btn-danger"
        ))
        self.assertTrue(del_btn.is_displayed())

        # Seed localStorage as if user had previously typed in the form. After
        # DEL, this entry should be cleared so the form doesn't repopulate
        # from stale state on the post-reload form render.
        self.browser.execute_script("""
            localStorage.setItem('sky-form:dashboard:sky',
                JSON.stringify({date:'1980-01-01', lat:'1', lon:'1', place:'Stale', tz:'UTC'}));
        """)

        del_btn.click()
        portal = self.wait_for(lambda: self.browser.find_element(By.ID, "id_guard_portal"))
        self.wait_for(lambda: self.assertIn("active", portal.get_attribute("class")))
        portal.find_element(By.CSS_SELECTOR, ".guard-yes").click()

        # 2. Page reloads → applet renders form (no chart_data)
        self.wait_for(lambda: self.browser.find_element(By.ID, "id_applet_sky_form_wrap"))

        # 3. localStorage cleared (so form isn't seeded with stale data)
        ls_value = self.browser.execute_script(
            "return localStorage.getItem('sky-form:dashboard:sky');"
        )
        self.assertIsNone(ls_value)

        # 4. DB-side: every sky field is cleared
        self.gamer.refresh_from_db()
        self.assertIsNone(self.gamer.sky_chart_data)
        self.assertIsNone(self.gamer.sky_birth_dt)
        self.assertEqual(self.gamer.sky_birth_place, "")


class MySkyAsyncSaveTest(FunctionalTest):
    """A fresh user (no saved sky) clicks SAVE SKY (mocked) — without a page
    reload, the page transitions into the saved state: body picks up
    .sky-saved, the wheel SVG renders, and the DEL btn becomes visible."""

    def setUp(self):
        super().setUp()
        Applet.objects.get_or_create(
            slug="my-sky",
            defaults={"name": "My Sky", "grid_cols": 6, "grid_rows": 6, "context": "dashboard"},
        )
        self.gamer = User.objects.create(email="newcomer@test.io")
        self.sky_url = self.live_server_url + "/dashboard/sky/"

    def test_save_sky_async_activates_saved_state_without_reload(self):
        self.create_pre_authenticated_session("newcomer@test.io")
        self.browser.get(self.sky_url)

        # Pre-save state: no wheel children, body has no sky-saved class
        body = self.browser.find_element(By.TAG_NAME, "body")
        self.assertNotIn("sky-saved", body.get_attribute("class"))

        # Mock /sky/preview + /sky/save
        self.browser.execute_script("""
            const FIXTURE = """ + _json.dumps(_CHART_FIXTURE) + """;
            window._origFetch = window.fetch;
            window.fetch = function(url, opts) {
                if (typeof url === 'string' && url.includes('/sky/preview')) {
                    return Promise.resolve({ok:true, json:()=>Promise.resolve(FIXTURE)});
                }
                if (typeof url === 'string' && url.includes('/sky/save')) {
                    return Promise.resolve({ok:true, json:()=>Promise.resolve({saved:true})});
                }
                return window._origFetch(url, opts);
            };
        """)

        # Fill required form fields & dispatch input → preview enables save btn
        self.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})
            );
        """)

        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()

        # 1. body picks up sky-saved without a reload
        self.wait_for(lambda: self.assertIn(
            "sky-saved",
            self.browser.find_element(By.TAG_NAME, "body").get_attribute("class"),
        ))

        # 2. Wheel SVG is now drawn (has child elements)
        self.wait_for(lambda: self.assertTrue(
            self.browser.find_elements(By.CSS_SELECTOR, "#id_sky_svg > *"),
            "Wheel SVG should have children after async save",
        ))

        # 3. DEL btn visible
        del_btn = self.browser.find_element(By.CSS_SELECTOR, ".sky-wheel-col .btn-danger")
        self.assertTrue(del_btn.is_displayed())


class MySkyDeleteFlowTest(FunctionalTest):
    """A .btn-danger DEL button sits at the wheel center when sky data exists.
    Clicking it summons the global #id_guard_portal (OK / NVM); OK clears all
    sky fields on the User model and reverts the page to the form-only view."""

    def setUp(self):
        super().setUp()
        Applet.objects.get_or_create(
            slug="my-sky",
            defaults={"name": "My Sky", "grid_cols": 6, "grid_rows": 6, "context": "dashboard"},
        )
        self.gamer = User.objects.create(email="stargazer@test.io")
        self.gamer.sky_chart_data  = _CHART_FIXTURE
        self.gamer.sky_birth_place = "Baltimore, MD, US"
        self.gamer.sky_birth_tz    = "America/New_York"
        self.gamer.sky_birth_lat   = 39.2904
        self.gamer.sky_birth_lon   = -76.6122
        self.gamer.sky_birth_dt    = datetime(
            1990, 6, 15, 12, 0, tzinfo=zoneinfo.ZoneInfo("UTC")
        )
        self.gamer.save()
        self.sky_url = self.live_server_url + "/dashboard/sky/"

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

    def test_del_btn_only_visible_when_sky_data_exists(self):
        """DEL btn is hidden for users w. no saved sky; visible once data is saved.
        The btn lives in the DOM unconditionally (so async save can reveal it) —
        visibility is gated by body.sky-saved via CSS."""
        self.create_pre_authenticated_session("stargazer@test.io")
        self.browser.get(self.sky_url)
        del_btn = self.wait_for(lambda: self.browser.find_element(
            By.CSS_SELECTOR, ".sky-wheel-col .btn-danger"
        ))
        self.assertTrue(del_btn.is_displayed())

        # Sanity: a fresh user (no chart_data) → DEL btn hidden (display:none)
        from apps.lyric.models import User as _U
        _U.objects.create(email="newcomer@test.io")
        self.create_pre_authenticated_session("newcomer@test.io")
        self.browser.get(self.sky_url)
        self.wait_for(lambda: self.browser.find_element(By.ID, "id_nf_date"))
        btns = self.browser.find_elements(By.CSS_SELECTOR, ".sky-wheel-col .btn-danger")
        self.assertTrue(
            not btns or not btns[0].is_displayed(),
            "DEL btn should not be visible before any sky data is saved",
        )

    # ── T2 ───────────────────────────────────────────────────────────────────

    def test_clicking_del_then_nvm_keeps_sky_data(self):
        """DEL → guard portal → NVM dismisses; user's sky fields are untouched."""
        self.create_pre_authenticated_session("stargazer@test.io")
        self.browser.get(self.sky_url)

        del_btn = self.wait_for(lambda: self.browser.find_element(
            By.CSS_SELECTOR, ".sky-wheel-col .btn-danger"
        ))
        del_btn.click()

        portal = self.wait_for(lambda: self.browser.find_element(By.ID, "id_guard_portal"))
        self.wait_for(lambda: self.assertIn("active", portal.get_attribute("class")))

        portal.find_element(By.CSS_SELECTOR, ".guard-no").click()
        self.wait_for(lambda: self.assertNotIn(
            "active", portal.get_attribute("class")
        ))

        self.gamer.refresh_from_db()
        self.assertIsNotNone(self.gamer.sky_chart_data)
        self.assertEqual(self.gamer.sky_birth_place, "Baltimore, MD, US")

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

    def test_clicking_del_then_ok_clears_sky_and_reverts_to_form_only(self):
        """DEL → guard portal → OK clears the user's sky fields, redirects, &
        the next sky-page render has no body.sky-saved class."""
        self.create_pre_authenticated_session("stargazer@test.io")
        self.browser.get(self.sky_url)

        # Confirm initial saved state
        body = self.browser.find_element(By.TAG_NAME, "body")
        self.assertIn("sky-saved", body.get_attribute("class"))

        del_btn = self.wait_for(lambda: self.browser.find_element(
            By.CSS_SELECTOR, ".sky-wheel-col .btn-danger"
        ))
        del_btn.click()

        portal = self.wait_for(lambda: self.browser.find_element(By.ID, "id_guard_portal"))
        self.wait_for(lambda: self.assertIn("active", portal.get_attribute("class")))

        portal.find_element(By.CSS_SELECTOR, ".guard-yes").click()

        # Page navigates back to /dashboard/sky/; body class no longer carries sky-saved
        self.wait_for(lambda: self.assertNotIn(
            "sky-saved",
            self.browser.find_element(By.TAG_NAME, "body").get_attribute("class"),
        ))

        # DB-side: every sky field is cleared
        self.gamer.refresh_from_db()
        self.assertIsNone(self.gamer.sky_chart_data)
        self.assertIsNone(self.gamer.sky_birth_dt)
        self.assertEqual(self.gamer.sky_birth_place, "")
        self.assertEqual(self.gamer.sky_birth_tz, "")


class MySkyWheelConjunctionTest(FunctionalTest):
    """Tick lines, z-raise, and dual tooltip for conjunct planets."""

    def setUp(self):
        super().setUp()
        Applet.objects.get_or_create(
            slug="my-sky",
            defaults={"name": "My Sky", "grid_cols": 6, "grid_rows": 6, "context": "dashboard"},
        )
        self.gamer = User.objects.create(email="stargazer@test.io")
        self.gamer.sky_chart_data  = _CHART_FIXTURE
        self.gamer.sky_birth_place = "Morganza, MD, US"
        self.gamer.save()

    def _load_wheel(self):
        self.create_pre_authenticated_session("stargazer@test.io")
        self.browser.get(self.live_server_url)
        self.wait_for(lambda: self.assertTrue(
            self.browser.find_element(By.CSS_SELECTOR, "#id_applet_my_sky .nw-root")
        ))

    # ── T6 ───────────────────────────────────────────────────────────────────

    def test_planet_tick_lines_present(self):
        """Every planet has one tick line in the SVG."""
        self._load_wheel()
        self.wait_for(lambda: self.assertEqual(
            len(self.browser.find_elements(
                By.CSS_SELECTOR, "#id_applet_my_sky .nw-planet-tick"
            )),
            10,
        ))

    # (T7 tick-extends-past-zodiac, T8 click-raises-to-front, and T9c/T9n/T9w
    # cycle navigation are covered by SkyWheelSpec.js.)
