"""Static UI tests for quieter tool-call rendering and shared design tokens.

These tests intentionally follow the repo's existing pytest style: read static
source files, isolate the relevant function/rule, and assert implementation
invariants before changing the UI.
"""
import pathlib
import re

REPO = pathlib.Path(__file__).parent.parent
UI_JS = (REPO / "static" / "ui.js").read_text(encoding="utf-8")
BOOT_JS = (REPO / "static" / "boot.js").read_text(encoding="utf-8")
CSS = (REPO / "static" / "style.css").read_text(encoding="utf-8")


def _function_body(src: str, name: str) -> str:
    match = re.search(rf"function\s+{re.escape(name)}\s*\(", src)
    assert match, f"{name}() not found"
    brace = src.find("{", match.end())
    assert brace != -1, f"{name}() has no body"
    depth = 1
    i = brace + 1
    in_string = None
    escaped = False
    in_line_comment = False
    in_block_comment = False
    while i < len(src) and depth:
        ch = src[i]
        nxt = src[i + 1] if i + 1 < len(src) else ""
        if in_line_comment:
            if ch == "\n":
                in_line_comment = False
            i += 1
            continue
        if in_block_comment:
            if ch == "*" and nxt == "/":
                in_block_comment = False
                i += 2
                continue
            i += 1
            continue
        if in_string:
            if escaped:
                escaped = False
            elif ch == "\\":
                escaped = True
            elif ch == in_string:
                in_string = None
            i += 1
            continue
        if ch == "/" and nxt == "/":
            in_line_comment = True
            i += 2
            continue
        if ch == "/" and nxt == "*":
            in_block_comment = True
            i += 2
            continue
        if ch in "'\"`":
            in_string = ch
            i += 1
            continue
        if ch == "{":
            depth += 1
        elif ch == "}":
            depth -= 1
        i += 1
    assert depth == 0, f"{name}() body did not close"
    return src[brace + 1:i - 1]


class TestToolCallGroupingStatic:
    def test_simplified_tool_calling_setting_is_wired_through_frontend(self):
        assert "settingsSimplifiedToolCalling" in (REPO / "static" / "index.html").read_text(encoding="utf-8"), (
            "Settings should expose a Compact tool activity checkbox."
        )
        assert "window._simplifiedToolCalling" in (REPO / "static" / "boot.js").read_text(encoding="utf-8"), (
            "Boot should hydrate simplified_tool_calling into a runtime flag."
        )
        panels = (REPO / "static" / "panels.js").read_text(encoding="utf-8")
        assert "settingsSimplifiedToolCalling" in panels and "simplified_tool_calling" in panels, (
            "Settings panel should load and save the simplified_tool_calling setting."
        )

    def test_simplified_tool_calling_autosave_hot_applies_renderer_mode(self):
        panels = (REPO / "static" / "panels.js").read_text(encoding="utf-8")
        fn = _function_body(panels, "_autosavePreferencesSettings")
        assert "window._simplifiedToolCalling" in fn, (
            "Autosaving Compact tool activity should update the live renderer flag immediately."
        )
        assert "clearMessageRenderCache()" in fn, (
            "Autosaving Compact tool activity should invalidate cached transcript HTML."
        )
        assert "renderMessages()" in fn, (
            "Autosaving Compact tool activity should rebuild the visible transcript without a refresh."
        )

    def test_render_messages_gates_settled_activity_grouping(self):
        fn = _function_body(UI_JS, "renderMessages")
        helper = _function_body(UI_JS, "ensureActivityGroup")
        assert "isSimplifiedToolCalling()" in fn, (
            "Settled tool/thinking grouping should be gated by the Compact tool activity toggle."
        )
        assert "tool-cards-toggle" in fn, (
            "The non-simplified path should preserve the upstream loose tool-card controls."
        )
        assert "data-tool-call-group" in helper, (
            "Tool-call groups need a stable data-tool-call-group attribute for CSS and tests."
        )
        assert re.search(r"cards\.length|toolCount|toolCalls\.length|group\.length", fn + helper), (
            "The simplified group header should derive its summary/count from the number of tool calls."
        )

    def test_tool_call_groups_default_collapsed_with_summary_visible(self):
        fn = _function_body(UI_JS, "renderMessages")
        helper = _function_body(UI_JS, "ensureActivityGroup")
        assert "tool-call-group-collapsed" in fn or "collapsed" in fn, (
            "Historical tool-call groups should default to a collapsed state."
        )
        assert "tool-call-group-summary" in helper, (
            "Collapsed groups must expose a visible summary/header row."
        )
        assert "tool-call-group-body" in helper, (
            "Tool-card detail rows should live inside a group body that can be "
            "expanded/collapsed."
        )
        assert "aria-expanded" in helper, (
            "The expand/collapse control must expose aria-expanded."
        )

    def test_activity_summary_omits_redundant_trailing_count_badge(self):
        helper = _function_body(UI_JS, "ensureActivityGroup")
        sync_fn = _function_body(UI_JS, "_syncToolCallGroupSummary")
        assert "tool-call-group-count" not in helper, (
            "Compact Activity summaries already state tool counts in the label; "
            "do not render a second trailing count badge."
        )
        assert "tool-call-group-count" not in sync_fn, (
            "The summary sync path should not update a hidden/removed trailing count badge."
        )

    def test_activity_summary_keeps_header_compact_without_tool_names_or_thinking_prefix(self):
        helper = _function_body(UI_JS, "ensureActivityGroup")
        sync_fn = _function_body(UI_JS, "_syncToolCallGroupSummary")
        assert "tool-call-group-list" not in helper, (
            "The compact Activity row should not allocate a secondary tool-name/thinking summary span."
        )
        assert "tool-call-group-list" not in sync_fn, (
            "The summary sync path should not populate a redundant tool-name/thinking list."
        )
        assert "Activity: thinking +" not in sync_fn, (
            "When tools are present, thinking is expected and should not be repeated in the label."
        )

    def test_live_tool_cards_use_grouping_only_when_simplified(self):
        live_fn = _function_body(UI_JS, "appendLiveToolCard")
        settled_fn = _function_body(UI_JS, "renderMessages")
        assert "isSimplifiedToolCalling()" in live_fn, (
            "Live streaming tool cards should branch on the Compact tool activity toggle."
        )
        assert "ensureActivityGroup" in live_fn, (
            "Compact live tool rendering should use the grouped activity container."
        )
        assert "toolRunningRow" in live_fn, (
            "The non-simplified live tool path should preserve the upstream running-dots row."
        )
        assert "buildToolCard" in live_fn and "buildToolCard" in settled_fn, (
            "Live and settled tool rendering should share buildToolCard() for consistent markup."
        )
        assert "data-live-tid" in live_fn, (
            "Live grouping must preserve data-live-tid so tool_start/tool_complete updates still replace the correct card."
        )

    def test_activity_disclosure_state_is_session_and_turn_scoped(self):
        helper = _function_body(UI_JS, "ensureActivityGroup")
        toggle_fn = _function_body(UI_JS, "_toggleActivityGroup")
        key_fn = _function_body(UI_JS, "_activityDisclosureStorageKey")
        render_fn = _function_body(UI_JS, "renderMessages")
        live_fn = _function_body(UI_JS, "appendLiveToolCard")
        thinking_fn = _function_body(UI_JS, "appendThinking")
        done_fn = (REPO / "static" / "messages.js").read_text(encoding="utf-8")
        assert "hermes-activity-disclosure:" in UI_JS, (
            "Activity disclosure state should use a dedicated localStorage namespace."
        )
        assert "S.session.session_id" in key_fn, (
            "Activity disclosure state must be scoped to the current chat/session."
        )
        assert "data-activity-disclosure-key" in helper, (
            "Each Activity group needs a stable per-turn key for persisted disclosure state."
        )
        assert "_readActivityDisclosureState" in helper, (
            "ensureActivityGroup() should hydrate the saved open/closed state before using defaults."
        )
        assert "_writeActivityDisclosureState" in toggle_fn, (
            "Clicking the Activity summary should persist the new open/closed state."
        )
        assert "assistant:" in render_fn, (
            "Settled Activity groups should be keyed by assistant message index."
        )
        assert "live:" in live_fn + thinking_fn, (
            "Live Activity groups should be keyed by active stream id."
        )
        assert "_copyActivityDisclosureState('live:'+streamId, 'assistant:'" in done_fn, (
            "When a live turn settles, its saved disclosure state should transfer to the persisted assistant turn."
        )

    def test_live_tool_activity_defaults_collapsed_unless_saved_open(self):
        live_fn = _function_body(UI_JS, "appendLiveToolCard")
        helper = _function_body(UI_JS, "ensureActivityGroup")
        assert "collapsed:false" not in re.sub(r"\s+", "", live_fn), (
            "Compact live tool activity should not force-open every time a chat is revisited."
        )
        assert "savedState==='open'" in helper or 'savedState==="open"' in helper, (
            "A previously-open Activity group should still restore open from persisted state."
        )

    def test_tools_and_thinking_share_one_collapsed_activity_dropdown(self):
        ui_min = re.sub(r"\s+", "", UI_JS)
        assert "functionensureActivityGroup(" in ui_min, (
            "Tool calls and thinking should share one agent-activity disclosure helper."
        )
        assert "data-agent-activity-group" in UI_JS, (
            "The shared tools/thinking disclosure needs a stable data-agent-activity-group hook."
        )
        assert "agent-activity-thinking" in UI_JS, (
            "Thinking content should be nested inside the shared activity dropdown, not rendered separately."
        )
        render_fn = _function_body(UI_JS, "renderMessages")
        assert "isSimplifiedToolCalling()" in render_fn and "assistantThinking.set(rawIdx, thinkingText)" in render_fn, (
            "Settled thinking should move into the shared activity dropdown only when Compact tool activity is enabled."
        )
        assert "seg.insertAdjacentHTML('beforeend', _thinkingCardHtml(thinkingText))" in render_fn, (
            "The non-simplified path should preserve standalone settled thinking cards."
        )

    def test_live_thinking_uses_shared_activity_dropdown_only_when_simplified(self):
        live_thinking_fn = _function_body(UI_JS, "appendThinking")
        assert "isSimplifiedToolCalling()" in live_thinking_fn, (
            "Live thinking should branch on the Compact tool activity toggle."
        )
        assert "ensureActivityGroup" in live_thinking_fn, (
            "Compact live thinking should be inserted into the shared activity dropdown."
        )
        assert "thinkingRow" in live_thinking_fn, (
            "The non-simplified live thinking path should preserve the upstream #thinkingRow card."
        )


class TestToolCardDesignTokens:
    def test_root_defines_shared_layout_design_tokens(self):
        for token in (
            "--radius-sm",
            "--radius-md",
            "--radius-card",
            "--space-1",
            "--space-2",
            "--space-3",
            "--font-size-xs",
            "--font-size-sm",
            "--surface-subtle",
            "--border-subtle",
        ):
            assert token in CSS, f"Missing design token {token} in style.css"

    def test_base_dark_palette_restores_upstream_gold_tokens(self):
        css_min = re.sub(r"\s+", "", CSS)
        expected_tokens = (
            "--bg:#0D0D1A",
            "--sidebar:#141425",
            "--border:#2A2A45",
            "--text:#FFF8DC",
            "--muted:#C0C0C0",
            "--accent:#FFD700",
            "--surface:#1A1A2E",
            "--topbar-bg:rgba(20,20,37,.98)",
        )
        for token in expected_tokens:
            assert token in css_min, f"Base dark palette token missing: {token}"

    def test_base_light_palette_restores_upstream_gold_tokens(self):
        css_min = re.sub(r"\s+", "", CSS)
        expected_tokens = (
            "--bg:#FEFCF7",
            "--sidebar:#FAF7F0",
            "--border:#E0D8C8",
            "--text:#1A1610",
            "--muted:#5C5344",
            "--accent:#B8860B",
            "--surface:#F3EEE3",
        )
        for token in expected_tokens:
            assert token in css_min, f"Base light palette token missing: {token}"

    def test_default_skin_preview_stays_upstream(self):
        boot_min = re.sub(r"\s+", "", BOOT_JS)
        assert "{name:'Default',colors:['#FFD700','#FFBF00','#CD7F32']}" in boot_min, (
            "The Default skin swatch should stay aligned with the upstream gold base."
        )

    def test_tool_card_css_uses_design_tokens_for_chrome(self):
        css_min = re.sub(r"\s+", "", CSS)
        assert ".tool-card{" in css_min, ".tool-card rule missing"
        assert "border-radius:var(--radius-card)" in css_min, (
            ".tool-card border radius should use --radius-card, not hardcoded px."
        )
        assert "background:var(--surface-subtle)" in css_min, (
            ".tool-card background should use --surface-subtle."
        )
        assert "border:1pxsolidvar(--border-subtle)" in css_min, (
            ".tool-card border should use --border-subtle."
        )

    def test_tool_card_header_and_text_use_spacing_and_font_tokens(self):
        css_min = re.sub(r"\s+", "", CSS)
        assert ".tool-card-header{" in css_min, ".tool-card-header rule missing"
        assert "gap:var(--space-2)" in css_min, (
            ".tool-card-header gap should use --space-2."
        )
        assert "padding:var(--space-1)var(--space-3)" in css_min, (
            ".tool-card-header padding should use spacing tokens."
        )
        assert ".tool-card-name{" in css_min and "font-size:var(--font-size-xs)" in css_min, (
            ".tool-card-name should use --font-size-xs."
        )
        assert ".tool-card-preview{" in css_min and "font-size:var(--font-size-xs)" in css_min, (
            ".tool-card-preview should use --font-size-xs."
        )
