"""Regression tests for the project chip UI fixes (issue #1085).

Two bugs:

1. The right-click context menu opened by `_showProjectContextMenu` was styled
   with `background: var(--panel)`, but `--panel` is NOT defined anywhere in
   style.css.  CSS falls back to `transparent` for undefined variables, so the
   menu appeared see-through and the session list bled through.  The fix
   replaces `var(--panel)` with `var(--surface)` — the same opaque variable
   used by `.session-action-menu` and other floating popovers.

2. The `.project-create-input` (used for both rename and new-project creation)
   had `width: 100px` hard-coded, so the field was always exactly 100px wide
   regardless of the project name being edited.  Fix: bound the field with
   `min-width: 40px` / `max-width: 180px` and `width: auto`, plus a
   `_resizeProjectInput()` JS helper that measures the current value with a
   hidden span and sets the pixel width accordingly.

These are static-source tests — CSS/JS behaviour of a popover and an input
sizer can't be exercised faithfully without a browser, but the patterns
worth pinning are the variable names, the absence of the bad ones, and the
presence of the resize helper at both call sites.
"""

import pathlib

REPO = pathlib.Path(__file__).parent.parent
SESSIONS_JS = (REPO / "static" / "sessions.js").read_text(encoding="utf-8")
STYLE_CSS = (REPO / "static" / "style.css").read_text(encoding="utf-8")


# ── Bug 1: context menu background ────────────────────────────────────────────


class TestContextMenuBackground:

    def test_panel_variable_not_defined_in_stylesheet(self):
        """`--panel` is not defined as a CSS custom property anywhere — so
        any rule using `var(--panel)` falls back to `transparent`, which is
        the actual root cause of the menu bleed-through.  This test
        documents that fact: if `--panel` is ever defined, the test will
        need updating but the fix is still safer using `--surface`."""
        # Match either ":root --panel:" or "--panel:" assignments; absence
        # confirms the fallback-to-transparent failure mode.
        assert "--panel:" not in STYLE_CSS, (
            "If --panel is now defined, update this test, but the menu "
            "should still use --surface for consistency with other popovers."
        )

    def test_context_menu_uses_surface_not_panel(self):
        """`_showProjectContextMenu` must set the menu background to
        `var(--surface)`, not `var(--panel)`."""
        # Locate the menu construction
        idx = SESSIONS_JS.find("project-ctx-menu")
        assert idx >= 0, "project-ctx-menu className not found in sessions.js"
        # Look at the surrounding 800 chars where the cssText is set
        window = SESSIONS_JS[idx: idx + 1200]
        assert "background:var(--surface)" in window, (
            "Project context menu must use background:var(--surface) for an "
            "opaque surface — var(--panel) is undefined and falls back to "
            "transparent."
        )
        assert "background:var(--panel)" not in window, (
            "Project context menu still uses background:var(--panel) — "
            "this CSS variable is not defined and renders transparent."
        )

    def test_session_action_menu_also_uses_surface_for_consistency(self):
        """Sanity check: the existing .session-action-menu (the analogous
        right-click menu for session items) uses `var(--surface)` — so the
        fix is consistent with the rest of the codebase."""
        assert "session-action-menu" in STYLE_CSS
        # Find the rule and confirm it uses --surface
        idx = STYLE_CSS.find(".session-action-menu")
        assert idx >= 0
        rule = STYLE_CSS[idx: idx + 400]
        assert "var(--surface)" in rule, (
            ".session-action-menu should use var(--surface) — kept here as "
            "the canonical reference for opaque popover surfaces."
        )


# ── Bug 2: project-create-input width ─────────────────────────────────────────


class TestProjectCreateInputWidth:

    def test_no_hardcoded_100px_width(self):
        """The fixed `width: 100px` on .project-create-input is gone."""
        idx = STYLE_CSS.find(".project-create-input{")
        assert idx >= 0, ".project-create-input rule not found in style.css"
        rule = STYLE_CSS[idx: idx + 400]
        assert "width:100px" not in rule and "width: 100px" not in rule, (
            "Fixed 100px width must be replaced with min-width/max-width/"
            "width:auto so the input grows with its content."
        )

    def test_min_and_max_width_present(self):
        """Both min-width and max-width must be set on .project-create-input."""
        idx = STYLE_CSS.find(".project-create-input{")
        rule = STYLE_CSS[idx: idx + 400]
        assert "min-width:40px" in rule, (
            f"min-width:40px not found in .project-create-input rule: {rule}"
        )
        assert "max-width:180px" in rule, (
            f"max-width:180px not found in .project-create-input rule: {rule}"
        )
        assert "width:auto" in rule, (
            f"width:auto not found in .project-create-input rule: {rule}"
        )


class TestResizeProjectInputHelper:
    """The `_resizeProjectInput` helper must exist and be wired into both
    rename and create call sites."""

    def test_resize_helper_defined(self):
        assert "function _resizeProjectInput(" in SESSIONS_JS, (
            "_resizeProjectInput helper not found in sessions.js"
        )

    def test_resize_helper_uses_hidden_span(self):
        """The standard pattern is to measure with a hidden absolute span
        sharing the same font/padding as the input. Font and family are read
        via getComputedStyle so the sizer stays calibrated if CSS changes."""
        idx = SESSIONS_JS.find("function _resizeProjectInput(")
        assert idx >= 0
        body = SESSIONS_JS[idx: idx + 900]
        assert "position:absolute" in body and "visibility:hidden" in body, (
            "_resizeProjectInput should use a hidden absolute span to "
            "measure the value's rendered width."
        )
        assert "getComputedStyle(inp)" in body, (
            "_resizeProjectInput should use getComputedStyle to read font "            "properties so the sizer stays calibrated if CSS changes."
        )
        assert "Math.min(180" in body, (
            "max bound (180) not applied in _resizeProjectInput"
        )
        assert "Math.max(40" in body, (
            "min bound (40) not applied in _resizeProjectInput"
        )

    def test_rename_calls_resize_helper(self):
        """`_startProjectRename` must call `_resizeProjectInput` once on
        creation and again on every input event."""
        idx = SESSIONS_JS.find("function _startProjectRename(")
        assert idx >= 0
        body = SESSIONS_JS[idx: idx + 1200]
        assert "_resizeProjectInput(inp)" in body, (
            "_startProjectRename must call _resizeProjectInput so the "
            "input width matches the existing project name."
        )
        # Wired into the input event so it grows as the user types
        assert "addEventListener('input'" in body and "_resizeProjectInput" in body, (
            "_startProjectRename must wire input events to _resizeProjectInput"
        )

    def test_create_calls_resize_helper(self):
        """Same for `_startProjectCreate` (new-project entry field)."""
        idx = SESSIONS_JS.find("function _startProjectCreate(")
        assert idx >= 0
        body = SESSIONS_JS[idx: idx + 1200]
        assert "_resizeProjectInput(inp)" in body, (
            "_startProjectCreate must call _resizeProjectInput on focus"
        )
        assert "addEventListener('input'" in body, (
            "_startProjectCreate must wire input events to _resizeProjectInput"
        )


class TestProjectChipLongPressTouch:
    """Mobile long-press to open the project context menu (#3760).

    Project chips were deletable only via the right-click context menu, which has
    no touch equivalent — so mobile users could never remove a project. A 500ms
    long-press now opens the same menu.
    """

    def _chip_touch_block(self):
        # The chip touch handlers live just after the oncontextmenu wiring in the
        # project-chip render loop.
        idx = SESSIONS_JS.find("Touch long-press")
        assert idx != -1, "project-chip long-press touch block not found"
        return SESSIONS_JS[idx: idx + 2300]

    def test_long_press_opens_project_context_menu(self):
        block = self._chip_touch_block()
        assert "addEventListener('touchstart'" in block
        assert "setTimeout(" in block and "},500);" in block
        assert "_showProjectContextMenu(" in block
        # visual feedback + scroll-drift cancel, mirroring the session-item pattern
        assert "long-pressing" in block
        assert "addEventListener('touchmove'" in block
        assert ">10" in block  # >10px drift cancels the press

    def test_long_press_suppresses_synthetic_click_and_filter_tap(self):
        block = self._chip_touch_block()
        # touchend must be non-passive so it can preventDefault the synthetic click
        assert "addEventListener('touchend'" in block
        assert "{passive:false}" in block
        assert "e.preventDefault();e.stopPropagation();" in block
        # the long-press handler cancels the pending single-tap filter timer
        assert "clearTimeout(_pClickTimer)" in block

    def test_touchstart_clears_inflight_timer_before_scheduling(self):
        """Regression: a second finger / stray touchstart must not orphan the
        prior timer (which would then fire the menu after the gesture was
        cancelled). touchstart clears any in-flight _lpTimer before scheduling,
        and the timer body bails if the gesture was already consumed.
        """
        block = self._chip_touch_block()
        # clear-before-schedule at the top of touchstart
        assert "if(_lpTimer){clearTimeout(_lpTimer);_lpTimer=null;}" in block, (
            "touchstart must clear any in-flight long-press timer before scheduling "
            "a new one (orphaned-timer fix)"
        )
        # stale-fire guard inside the timer body
        assert "if(_lpHandled) return;" in block, (
            "the long-press timer body must no-op if the gesture was already consumed"
        )

    def test_long_pressing_style_feedback_present(self):
        assert ".project-chip.long-pressing" in STYLE_CSS
        # Target the base .project-chip rule (the one carrying the layout props),
        # not an unrelated theme override of the same selector.
        base_idx = STYLE_CSS.find(".project-chip{font-size")
        assert base_idx != -1, "base .project-chip rule not found"
        chip_rule = STYLE_CSS[base_idx: STYLE_CSS.find("}", base_idx) + 1]
        # touch tuning so the native callout/selection doesn't compete with the gesture
        assert "touch-action:manipulation" in chip_rule
        assert "user-select:none" in chip_rule


class TestQuickCreateMobileDrawer:
    """The project-chip "+" (quick-create) must close the mobile sidebar drawer.

    On phones the sidebar is a full-screen drawer (`.sidebar.mobile-open`,
    z-index 200) covering the main chat view. The quick-create button creates
    the new project conversation but never closed the drawer, so on mobile the
    tap *looked* like a no-op — the session was created, just hidden underneath.
    Mirrors `$('btnNewChat').onclick` in boot.js and the #5409 close in
    `_openSidebarSession`.
    """

    def _quick_create_block(self):
        idx = SESSIONS_JS.find("function _attachProjectQuickCreateButton(")
        assert idx != -1, "project quick-create helper not found in sessions.js"
        # Function body incl. the full onclick success path (~3K covers it).
        return SESSIONS_JS[idx: idx + 3000]

    def test_quick_create_success_path_closes_mobile_drawer(self):
        block = self._quick_create_block()
        assert "closeMobileSidebar" in block, (
            "project quick-create + must close the mobile sidebar after "
            "newSession so the new conversation is visible on phones"
        )
        # Guarded call, matching the _openSidebarSession (#5409) convention.
        assert "typeof closeMobileSidebar==='function'" in block

    def test_quick_create_close_runs_after_sidebar_repaint(self):
        """The close must sit in the success path after the sidebar repaint —
        not before newSession (the drawer would reopen over the pending load)
        and not in the catch branch (failure should keep the drawer open so the
        user can see the toast and retry)."""
        block = self._quick_create_block()
        render_idx = block.find("renderSessionList({deferWhileInteracting:false})")
        close_idx = block.find("closeMobileSidebar")
        catch_idx = block.find("catch(err)")
        assert render_idx != -1, "sidebar repaint call not found in quick-create handler"
        assert close_idx != -1, "closeMobileSidebar not found in quick-create handler"
        assert catch_idx != -1, "success-path try/catch not found in quick-create handler"
        assert close_idx > render_idx, (
            "closeMobileSidebar must run after the sidebar repaint in the success path"
        )
        assert close_idx < catch_idx, (
            "closeMobileSidebar must stay in the success path (try block) — moving it "
            "into the catch branch would keep the drawer open over the error toast"
        )

