"""Room scroll-of-events FT — the table-hex aperture's binary scroll-snap
toggle (mirrors my_sky's wheel<->form swap). From Role Select onwards the
viewer scrolls DOWN past the hex to reveal the room's provenance feed (the
same GameEvent stream the Billscroll page shows); scrolling back up snaps to
the hex. No partial scroll — `scroll-snap-stop: always` enforces the binary.

FT bucket: game_room. Built to DRY-template onto my_sea next.
"""
from selenium.webdriver.common.by import By

from .base import FunctionalTest
from .room_page import _equip_earthman_deck, _fill_room_via_orm
from apps.drama.models import GameEvent
from apps.epic.models import Room
from apps.lyric.models import User


class RoomScrollOfEventsTest(FunctionalTest):
    def setUp(self):
        super().setUp()
        self.viewer = User.objects.create(email="disco@test.io", username="disco")
        _equip_earthman_deck(self.viewer)
        self.room = Room.objects.create(name="Whataburgher", owner=self.viewer)
        _fill_room_via_orm(
            self.room,
            ["disco@test.io", "amigo@test.io", "bud@test.io",
             "pal@test.io", "dude@test.io", "bro@test.io"],
        )
        # A deposit line for the feed (data-label="frame") — renders via the
        # shared scroll partial as "deposits a Carte Blanche for slot 1 …".
        GameEvent.objects.create(
            room=self.room, actor=self.viewer, verb=GameEvent.SLOT_FILLED,
            data={"token_type": "carte", "token_display": "Carte Blanche",
                  "slot_number": 1, "renewal_days": 7},
        )
        # A retracted SIG_READY (struck → data-label="redact") so the Frame /
        # Redact filter has both kinds of row to toggle.
        GameEvent.objects.create(
            room=self.room, actor=self.viewer, verb=GameEvent.SIG_READY,
            data={"retracted": True, "card_name": "The Nomad"},
        )
        self.room.table_status = Room.ROLE_SELECT
        self.room.gate_status = Room.OPEN
        self.room.save()
        self.url = self.live_server_url + f"/gameboard/room/{self.room.id}/"

    def _open(self):
        self.create_pre_authenticated_session("disco@test.io")
        self.browser.set_window_size(820, 600)
        self.browser.get(self.url)
        return self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, ".room-aperture")
        )

    def _in_viewport(self, css):
        return self.browser.execute_script(
            "var e=document.querySelector(arguments[0]); if(!e) return false;"
            "var r=e.getBoundingClientRect();"
            "return r.top < window.innerHeight && r.bottom > 0;", css)

    def test_aperture_binary_scroll_snap_with_two_panes(self):
        """The aperture is scroll-snap-y-mandatory and both panes snap to the
        start with `always` — without these it's a free scroll, not the binary
        hex<->scroll toggle."""
        self._open()
        snap = self.browser.execute_script(
            "return getComputedStyle(document.querySelector('.room-aperture'))"
            ".scrollSnapType;"
        )
        self.assertIn("y", snap)
        self.assertIn("mandatory", snap)
        for pane in (".room-hex-pane", ".room-scroll-pane"):
            align = self.browser.execute_script(
                "return getComputedStyle(document.querySelector(arguments[0]))"
                ".scrollSnapAlign;", pane,
            )
            self.assertIn("start", align)

    def test_scroll_down_reveals_event_feed(self):
        """The feed sits below the hex (off-screen); scrolling the aperture
        down brings it into view — the hex content is replaced entirely."""
        self._open()
        # The event feed starts off-screen, below the hex pane.
        self.assertFalse(self._in_viewport("#id_drama_scroll"))

        self.browser.execute_script(
            "document.querySelector('.room-aperture').scrollTop = 99999;"
        )
        # The aperture actually scrolled to the 2nd snap section…
        self.wait_for(lambda: self.assertGreater(
            self.browser.execute_script(
                "return document.querySelector('.room-aperture').scrollTop;"
            ),
            10,
        ))
        # …and the provenance feed (with the deposit line) is now in view.
        self.wait_for(lambda: self.assertTrue(self._in_viewport("#id_drama_scroll")))
        self.assertIn(
            "deposits a Carte Blanche for slot 1",
            self.browser.find_element(By.ID, "id_drama_scroll").text,
        )

    def _open_gear(self):
        gear = self.browser.find_element(
            By.CSS_SELECTOR, ".gear-btn[data-menu-target='id_room_menu']")
        self.browser.execute_script("arguments[0].click();", gear)

    def test_gear_swaps_to_filter_when_scrolled_to_feed(self):
        """At the hex the gear shows the room menu (NVM/DEL/BYE); scrolled to
        the feed it swaps to the Frame/Redact filter."""
        self._open()
        # Hex view: open the gear → the room menu is visible, the filter isn't.
        self._open_gear()
        self.wait_for(lambda: self.assertTrue(
            self.browser.find_element(
                By.CSS_SELECTOR, "#id_room_menu .room-menu-default a.btn-cancel"
            ).is_displayed()))
        self.assertFalse(
            self.browser.find_element(By.ID, "id_scroll_filter_form").is_displayed())

        # Scroll to the feed → the gear live-swaps to the filter pane.
        self.browser.execute_script(
            "document.querySelector('.room-aperture').scrollTop = 99999;")
        self.wait_for(lambda: self.assertTrue(
            self.browser.find_element(By.ID, "id_scroll_filter_form").is_displayed()))
        self.assertFalse(
            self.browser.find_element(
                By.CSS_SELECTOR, "#id_room_menu .room-menu-default a.btn-cancel"
            ).is_displayed())

    def test_scroll_swaps_room_title_to_scroll(self):
        """Scrolling the aperture to the feed advances the page-title reel
        GAME ROOM → GAME SCROLL (the <h2> gains `.is-scroll`, which the reel
        CSS uses to slide the words); scrolling back to the hex reverses it."""
        self._open()
        # Both reel words ship in the header; the window starts at the hex.
        self.assertTrue(
            self.browser.find_elements(By.CSS_SELECTOR, ".gr-word--base"))
        self.assertTrue(
            self.browser.find_elements(By.CSS_SELECTOR, ".gr-word--scroll"))
        h2 = self.browser.find_element(By.CSS_SELECTOR, ".row .col-lg-6 h2")
        self.assertNotIn("is-scroll", h2.get_attribute("class") or "")

        # Scroll down to the feed → the reel advances to GAME SCROLL.
        self.browser.execute_script(
            "document.querySelector('.room-aperture').scrollTop = 99999;")
        self.wait_for(lambda: self.assertIn(
            "is-scroll",
            self.browser.find_element(
                By.CSS_SELECTOR, ".row .col-lg-6 h2"
            ).get_attribute("class") or "",
        ))

        # Scroll back up to the hex → the reel reverses to GAME ROOM.
        self.browser.execute_script(
            "document.querySelector('.room-aperture').scrollTop = 0;")
        self.wait_for(lambda: self.assertNotIn(
            "is-scroll",
            self.browser.find_element(
                By.CSS_SELECTOR, ".row .col-lg-6 h2"
            ).get_attribute("class") or "",
        ))

    def test_redact_filter_hides_struck_rows(self):
        """Unchecking Redact + OK in the scroll-view gear hides the struck
        (retracted) rows while the framed ones stay."""
        self._open()
        self.browser.execute_script(
            "document.querySelector('.room-aperture').scrollTop = 99999;")
        self._open_gear()
        self.wait_for(lambda: self.assertTrue(
            self.browser.find_element(By.ID, "id_scroll_filter_form").is_displayed()))

        redact_cb = self.browser.find_element(
            By.CSS_SELECTOR, "#id_scroll_filter_form input[value='redact']")
        if redact_cb.is_selected():
            self.browser.execute_script("arguments[0].click();", redact_cb)
        ok = self.browser.find_element(
            By.CSS_SELECTOR, "#id_scroll_filter_form button[type='submit']")
        self.browser.execute_script("arguments[0].click();", ok)

        self.wait_for(lambda: self.assertFalse(
            self.browser.find_element(
                By.CSS_SELECTOR,
                "#id_drama_scroll .drama-event[data-label='redact']",
            ).is_displayed()))
        self.assertTrue(
            self.browser.find_element(
                By.CSS_SELECTOR,
                "#id_drama_scroll .drama-event[data-label='frame']",
            ).is_displayed())

# NOTE: the SCROLL applet's live WS refresh (record() → on_commit broadcast →
# RoomConsumer.scroll_update relay → room-scroll.js re-fetch + swap) has no FT.
# ChannelsLiveServerTestCase runs daphne in a SEPARATE PROCESS, and the test
# settings force the in-memory channel layer, so a group_send issued from the
# test process can never reach the consumer in the daphne process — the nudge is
# undeliverable regardless of the (correct) feature. Production uses Redis
# (cross-process) so it works live there. Every link is covered by ITs instead:
#   - apps.drama RecordBroadcast: record() schedules the broadcast on_commit
#     (and not before) — test_record_broadcasts_scroll_update_on_commit
#   - apps.epic RoomConsumer relays scroll_update — test_receives_scroll_update_broadcast
#   - apps.epic ScrollStatusViewTest: the re-fetched feed partial renders/reflects
#     the latest events
# (and room-scroll.js's fetch+swap of #id_drama_scroll was hand-verified.)
