"""Automated Playwright tests for SX docs demos.

Covers hypermedia examples, reactive islands, and marshes.
Run against the dev server:

    cd sx/tests
    pytest test_demos.py -v --headed   # visible browser
    pytest test_demos.py -v            # headless

Requires: pip install pytest-playwright
"""
from __future__ import annotations

import pytest
from playwright.sync_api import Page, expect

import os
BASE = os.environ.get("SX_TEST_BASE", "http://localhost:8013")


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

@pytest.fixture(scope="session")
def browser_context_args():
    return {"ignore_https_errors": True}


def nav(page: Page, path: str):
    """Navigate to an SX URL and wait for rendered content.

    Captures JS errors during page load and fails immediately with the
    actual error message instead of waiting for a 30s timeout.
    """
    js_errors: list[str] = []
    page.on("pageerror", lambda err: js_errors.append(str(err)))

    page.goto(f"{BASE}/sx/{path}", wait_until="domcontentloaded")

    # Wait for SX runtime hydration to complete
    page.wait_for_selector("html[data-sx-ready]", timeout=15000)

    # Fail fast on JS errors — don't wait for content that will never appear
    if js_errors:
        pytest.fail(f"JS error on {path}: {js_errors[0]}")

    # Wait for SX to render — look for any heading or paragraph in main panel
    page.wait_for_selector("#main-panel h2, #main-panel p, #main-panel div", timeout=10000)

    # Check for rendering artifacts that indicate broken serialization
    main_text = page.locator("#main-panel").text_content() or ""
    if "[object Object]" in main_text:
        pytest.fail(f"Rendering artifact on {path}: [object Object] in page content")


# ---------------------------------------------------------------------------
# Hypermedia Examples
# ---------------------------------------------------------------------------

class TestClickToLoad:
    def test_page_loads(self, page: Page):
        nav(page, "(geography.(hypermedia.(example.click-to-load)))")
        expect(page.locator("#main-panel")).to_contain_text("Click to Load")

    def test_click_loads_content(self, page: Page):
        nav(page, "(geography.(hypermedia.(example.click-to-load)))")
        page.click("button:has-text('Load content')")
        expect(page.locator("#click-result")).to_contain_text(
            "Content loaded", timeout=5000
        )


class TestFormSubmission:
    def test_page_loads(self, page: Page):
        nav(page, "(geography.(hypermedia.(example.form-submission)))")
        expect(page.locator("#main-panel")).to_contain_text("Form Submission")

    def test_has_code_examples(self, page: Page):
        """Page must show component source and handler source code."""
        nav(page, "(geography.(hypermedia.(example.form-submission)))")
        code_blocks = page.locator("pre code")
        expect(code_blocks.first).not_to_be_empty(timeout=5000)
        # Should have at least component code and handler code
        count = code_blocks.count()
        assert count >= 2, f"Expected at least 2 code blocks, got {count}"
        # Handler code should contain defhandler
        all_code = " ".join(code_blocks.nth(i).text_content() or ""
                            for i in range(count))
        assert "defhandler" in all_code or "sx-post" in all_code, \
            f"Code blocks should contain handler or component source"

    def test_submit_form(self, page: Page):
        nav(page, "(geography.(hypermedia.(example.form-submission)))")
        page.fill("input[name='name']", "TestUser")
        page.click("button[type='submit']")
        expect(page.locator("#form-result")).to_contain_text("TestUser", timeout=5000)


class TestPolling:
    def test_page_loads(self, page: Page):
        nav(page, "(geography.(hypermedia.(example.polling)))")
        expect(page.locator("#main-panel")).to_contain_text("Polling")

    def test_poll_updates(self, page: Page):
        nav(page, "(geography.(hypermedia.(example.polling)))")
        expect(page.locator("#poll-target")).to_contain_text("Server time", timeout=10000)


class TestDeleteRow:
    def test_page_loads(self, page: Page):
        nav(page, "(geography.(hypermedia.(example.delete-row)))")
        expect(page.locator("#main-panel")).to_contain_text("Delete Row")

    def test_delete_removes_row(self, page: Page):
        nav(page, "(geography.(hypermedia.(example.delete-row)))")
        rows = page.locator("#delete-rows tr")
        initial_count = rows.count()
        assert initial_count >= 2, f"Expected at least 2 rows, got {initial_count}"
        # Accept the confirm dialog
        page.on("dialog", lambda d: d.accept())
        rows.first.locator("button:has-text('delete')").click()
        expect(rows).to_have_count(initial_count - 1, timeout=5000)


class TestInlineEdit:
    def test_page_loads(self, page: Page):
        nav(page, "(geography.(hypermedia.(example.inline-edit)))")
        expect(page.locator("#main-panel")).to_contain_text("Inline Edit")

    def test_edit_shows_form(self, page: Page):
        nav(page, "(geography.(hypermedia.(example.inline-edit)))")
        page.click("#edit-target button:has-text('edit')")
        expect(page.locator("input[name='value']")).to_be_visible(timeout=5000)


class TestOobSwaps:
    def test_page_loads(self, page: Page):
        nav(page, "(geography.(hypermedia.(example.oob-swaps)))")
        expect(page.locator("#main-panel")).to_contain_text("OOB")

    def test_oob_updates_both_boxes(self, page: Page):
        nav(page, "(geography.(hypermedia.(example.oob-swaps)))")
        page.click("button:has-text('Update both boxes')")
        expect(page.locator("#oob-box-a")).to_contain_text("Box A updated", timeout=5000)
        expect(page.locator("#oob-box-b").last).to_contain_text("Box B updated", timeout=5000)


class TestLazyLoading:
    def test_page_loads(self, page: Page):
        nav(page, "(geography.(hypermedia.(example.lazy-loading)))")
        expect(page.locator("#main-panel")).to_contain_text("Lazy Loading")

    def test_content_loads_automatically(self, page: Page):
        nav(page, "(geography.(hypermedia.(example.lazy-loading)))")
        expect(page.locator("#lazy-target")).to_contain_text("Content loaded", timeout=10000)


class TestInfiniteScroll:
    def test_page_loads(self, page: Page):
        nav(page, "(geography.(hypermedia.(example.infinite-scroll)))")
        expect(page.locator("#main-panel")).to_contain_text("Infinite Scroll")


class TestProgressBar:
    def test_page_loads(self, page: Page):
        nav(page, "(geography.(hypermedia.(example.progress-bar)))")
        expect(page.locator("#main-panel")).to_contain_text("Progress Bar")

    def test_progress_starts(self, page: Page):
        nav(page, "(geography.(hypermedia.(example.progress-bar)))")
        page.click("button:has-text('Start job')")
        expect(page.locator("#progress-target")).to_contain_text("complete", timeout=10000)


class TestActiveSearch:
    def test_page_loads(self, page: Page):
        nav(page, "(geography.(hypermedia.(example.active-search)))")
        expect(page.locator("#main-panel")).to_contain_text("Active Search")

    def test_search_returns_results(self, page: Page):
        nav(page, "(geography.(hypermedia.(example.active-search)))")
        page.locator("input[name='q']").type("Py")
        expect(page.locator("#search-results")).to_contain_text("Python", timeout=5000)


class TestInlineValidation:
    def test_page_loads(self, page: Page):
        nav(page, "(geography.(hypermedia.(example.inline-validation)))")
        expect(page.locator("#main-panel")).to_contain_text("Inline Validation")

    def test_invalid_email(self, page: Page):
        nav(page, "(geography.(hypermedia.(example.inline-validation)))")
        page.fill("input[name='email']", "notanemail")
        page.locator("input[name='email']").blur()
        expect(page.locator("#email-feedback")).to_contain_text("Invalid", timeout=5000)


class TestValueSelect:
    def test_page_loads(self, page: Page):
        nav(page, "(geography.(hypermedia.(example.value-select)))")
        expect(page.locator("#main-panel")).to_contain_text("Value Select")

    def test_select_changes_options(self, page: Page):
        nav(page, "(geography.(hypermedia.(example.value-select)))")
        page.select_option("select[name='category']", "Languages")
        expect(page.locator("#value-items")).to_contain_text("Python", timeout=5000)


class TestResetOnSubmit:
    def test_page_loads(self, page: Page):
        nav(page, "(geography.(hypermedia.(example.reset-on-submit)))")
        expect(page.locator("#main-panel")).to_contain_text("Reset")

    def test_has_form(self, page: Page):
        nav(page, "(geography.(hypermedia.(example.reset-on-submit)))")
        expect(page.locator("form")).to_be_visible(timeout=5000)


class TestEditRow:
    def test_page_loads(self, page: Page):
        nav(page, "(geography.(hypermedia.(example.edit-row)))")
        expect(page.locator("#main-panel")).to_contain_text("Edit Row")


class TestBulkUpdate:
    def test_page_loads(self, page: Page):
        nav(page, "(geography.(hypermedia.(example.bulk-update)))")
        expect(page.locator("#main-panel")).to_contain_text("Bulk Update")

    def test_has_checkboxes(self, page: Page):
        nav(page, "(geography.(hypermedia.(example.bulk-update)))")
        expect(page.locator("input[type='checkbox']").first).to_be_visible(timeout=5000)


class TestSwapPositions:
    def test_page_loads(self, page: Page):
        nav(page, "(geography.(hypermedia.(example.swap-positions)))")
        expect(page.locator("#main-panel")).to_contain_text("Swap")


class TestSelectFilter:
    def test_page_loads(self, page: Page):
        nav(page, "(geography.(hypermedia.(example.select-filter)))")
        expect(page.locator("#main-panel")).to_contain_text("Select Filter")


class TestTabs:
    def test_page_loads(self, page: Page):
        nav(page, "(geography.(hypermedia.(example.tabs)))")
        expect(page.locator("#main-panel")).to_contain_text("Tabs")


class TestAnimations:
    def test_page_loads(self, page: Page):
        nav(page, "(geography.(hypermedia.(example.animations)))")
        expect(page.locator("#main-panel")).to_contain_text("Animation")


class TestDialogs:
    def test_page_loads(self, page: Page):
        nav(page, "(geography.(hypermedia.(example.dialogs)))")
        expect(page.locator("#main-panel")).to_contain_text("Dialog")


class TestKeyboardShortcuts:
    def test_page_loads(self, page: Page):
        nav(page, "(geography.(hypermedia.(example.keyboard-shortcuts)))")
        expect(page.locator("#main-panel")).to_contain_text("Keyboard")


class TestPutPatch:
    def test_page_loads(self, page: Page):
        nav(page, "(geography.(hypermedia.(example.put-patch)))")
        expect(page.locator("#main-panel")).to_contain_text("PUT")


class TestJsonEncoding:
    def test_page_loads(self, page: Page):
        nav(page, "(geography.(hypermedia.(example.json-encoding)))")
        expect(page.locator("#main-panel")).to_contain_text("JSON")


class TestValsAndHeaders:
    def test_page_loads(self, page: Page):
        nav(page, "(geography.(hypermedia.(example.vals-and-headers)))")
        expect(page.locator("#main-panel")).to_contain_text("Vals")


class TestLoadingStates:
    def test_page_loads(self, page: Page):
        nav(page, "(geography.(hypermedia.(example.loading-states)))")
        expect(page.locator("#main-panel")).to_contain_text("Loading")


class TestSyncReplace:
    def test_page_loads(self, page: Page):
        nav(page, "(geography.(hypermedia.(example.sync-replace)))")
        expect(page.locator("#main-panel")).to_contain_text("Request Abort")


class TestRetry:
    def test_page_loads(self, page: Page):
        nav(page, "(geography.(hypermedia.(example.retry)))")
        expect(page.locator("#main-panel")).to_contain_text("Retry")


# ---------------------------------------------------------------------------
# Reactive Islands
# ---------------------------------------------------------------------------

class TestReactiveIslandsOverview:
    def test_page_loads(self, page: Page):
        nav(page, "(geography.(reactive))")
        expect(page.locator("#main-panel")).to_contain_text("Reactive Islands")

    def test_architecture_table(self, page: Page):
        nav(page, "(geography.(reactive))")
        expect(page.locator("table").first).to_be_visible()


class TestReactiveExamplesOverview:
    def test_page_loads(self, page: Page):
        nav(page, "(geography.(reactive.(examples)))")
        expect(page.locator("#main-panel")).to_contain_text("Examples")


class TestReactiveCounter:
    def test_page_loads(self, page: Page):
        nav(page, "(geography.(reactive.(examples.counter)))")
        expect(page.locator("#main-panel")).to_contain_text("signal holds a value")

    def test_counter_increments(self, page: Page):
        nav(page, "(geography.(reactive.(examples.counter)))")
        # Find the island's + button
        island = page.locator("[data-sx-island*='demo-counter']")
        expect(island).to_be_visible(timeout=5000)
        # Wait for hydration
        page.wait_for_timeout(1000)
        initial = island.locator("span.text-2xl").text_content()
        island.locator("button", has_text="+").click()
        page.wait_for_timeout(500)
        updated = island.locator("span.text-2xl").text_content()
        assert initial != updated, f"Counter should change: {initial} -> {updated}"


class TestReactiveTemperature:
    def test_page_loads(self, page: Page):
        nav(page, "(geography.(reactive.(examples.temperature)))")
        expect(page.locator("#main-panel")).to_contain_text("Temperature")

    def test_temperature_updates(self, page: Page):
        nav(page, "(geography.(reactive.(examples.temperature)))")
        island = page.locator("[data-sx-island*='demo-temperature']")
        expect(island).to_be_visible(timeout=5000)
        page.wait_for_timeout(1000)
        island.locator("button", has_text="+5").click()
        page.wait_for_timeout(500)
        # Should show celsius value > 20 (default)
        expect(island).to_contain_text("25")


class TestReactiveStopwatch:
    def test_page_loads(self, page: Page):
        nav(page, "(geography.(reactive.(examples.stopwatch)))")
        expect(page.locator("#main-panel")).to_contain_text("Stopwatch")


class TestReactiveImperative:
    def test_page_loads(self, page: Page):
        nav(page, "(geography.(reactive.(examples.imperative)))")
        expect(page.locator("#main-panel")).to_contain_text("Imperative")


class TestReactiveList:
    def test_page_loads(self, page: Page):
        nav(page, "(geography.(reactive.(examples.reactive-list)))")
        expect(page.locator("#main-panel")).to_contain_text("Reactive List")

    def test_add_item(self, page: Page):
        nav(page, "(geography.(reactive.(examples.reactive-list)))")
        island = page.locator("[data-sx-island*='demo-reactive-list']")
        expect(island).to_be_visible(timeout=5000)
        page.wait_for_timeout(1000)
        island.locator("button", has_text="Add Item").click()
        page.wait_for_timeout(500)
        expect(island).to_contain_text("Item 1")


class TestReactiveInputBinding:
    def test_page_loads(self, page: Page):
        nav(page, "(geography.(reactive.(examples.input-binding)))")
        expect(page.locator("#main-panel")).to_contain_text("Input Binding")

    def test_input_binding(self, page: Page):
        nav(page, "(geography.(reactive.(examples.input-binding)))")
        island = page.locator("[data-sx-island*='demo-input-binding']")
        expect(island).to_be_visible(timeout=5000)
        page.wait_for_timeout(1000)
        island.locator("input[type='text']").fill("World")
        page.wait_for_timeout(500)
        expect(island).to_contain_text("Hello, World!")


class TestReactivePortal:
    def test_page_loads(self, page: Page):
        nav(page, "(geography.(reactive.(examples.portal)))")
        expect(page.locator("#main-panel")).to_contain_text("Portal")


class TestReactiveErrorBoundary:
    def test_page_loads(self, page: Page):
        nav(page, "(geography.(reactive.(examples.error-boundary)))")
        expect(page.locator("#main-panel")).to_contain_text("Error")


class TestReactiveRefs:
    def test_page_loads(self, page: Page):
        nav(page, "(geography.(reactive.(examples.refs)))")
        expect(page.locator("#main-panel")).to_contain_text("Refs")


class TestReactiveDynamicClass:
    def test_page_loads(self, page: Page):
        nav(page, "(geography.(reactive.(examples.dynamic-class)))")
        expect(page.locator("#main-panel")).to_contain_text("Dynamic")


class TestReactiveResource:
    def test_page_loads(self, page: Page):
        nav(page, "(geography.(reactive.(examples.resource)))")
        expect(page.locator("#main-panel")).to_contain_text("Resource")


class TestReactiveTransition:
    def test_page_loads(self, page: Page):
        nav(page, "(geography.(reactive.(examples.transition)))")
        expect(page.locator("#main-panel")).to_contain_text("Transition")


class TestReactiveStores:
    def test_page_loads(self, page: Page):
        nav(page, "(geography.(reactive.(examples.stores)))")
        expect(page.locator("#main-panel")).to_contain_text("Store")


class TestReactiveEventBridge:
    def test_page_loads(self, page: Page):
        nav(page, "(geography.(reactive.(examples.event-bridge-demo)))")
        expect(page.locator("#main-panel")).to_contain_text("Event Bridge")


class TestReactiveDefisland:
    def test_page_loads(self, page: Page):
        nav(page, "(geography.(reactive.(examples.defisland)))")
        expect(page.locator("#main-panel")).to_contain_text("defisland")


class TestReactiveTests:
    def test_page_loads(self, page: Page):
        nav(page, "(geography.(reactive.(examples.tests)))")
        expect(page.locator("#main-panel")).to_contain_text("test")


class TestReactiveCoverage:
    def test_page_loads(self, page: Page):
        nav(page, "(geography.(reactive.(examples.coverage)))")
        expect(page.locator("#main-panel")).to_contain_text("React")


# ---------------------------------------------------------------------------
# Marshes
# ---------------------------------------------------------------------------

class TestMarshesOverview:
    def test_page_loads(self, page: Page):
        nav(page, "(geography.(marshes))")
        expect(page.locator("#main-panel")).to_contain_text("Marshes")


class TestMarshesHypermediaFeeds:
    def test_page_loads(self, page: Page):
        nav(page, "(geography.(marshes.hypermedia-feeds))")
        expect(page.locator("#main-panel")).to_contain_text("Hypermedia Feeds")


class TestMarshesServerSignals:
    def test_page_loads(self, page: Page):
        nav(page, "(geography.(marshes.server-signals))")
        expect(page.locator("#main-panel")).to_contain_text("Server Writes")


class TestMarshesOnSettle:
    def test_page_loads(self, page: Page):
        nav(page, "(geography.(marshes.on-settle))")
        expect(page.locator("#main-panel")).to_contain_text("sx-on-settle")


class TestMarshesSignalTriggers:
    def test_page_loads(self, page: Page):
        nav(page, "(geography.(marshes.signal-triggers))")
        expect(page.locator("#main-panel")).to_contain_text("Signal-Bound")


class TestMarshesViewTransform:
    def test_page_loads(self, page: Page):
        nav(page, "(geography.(marshes.view-transform))")
        expect(page.locator("#main-panel")).to_contain_text("Reactive View")


# ---------------------------------------------------------------------------
# Spec Explorer
# ---------------------------------------------------------------------------

class TestSpecExplorer:
    def test_evaluator_server_renders(self, page: Page):
        """Server returns spec explorer content with evaluator data."""
        resp = page.request.get(f"{BASE}/sx/(language.(spec.(explore.evaluator)))")
        assert resp.ok, f"Server returned {resp.status}"
        body = resp.text()
        assert "Evaluator" in body, "Should contain evaluator title"
        assert "eval" in body.lower(), "Should contain evaluator content"

    def test_parser_server_renders(self, page: Page):
        """Server returns spec explorer content with parser data."""
        resp = page.request.get(f"{BASE}/sx/(language.(spec.(explore.parser)))")
        assert resp.ok, f"Server returned {resp.status}"
        body = resp.text()
        assert "Parser" in body, "Should contain parser title"

    def test_has_spec_source(self, page: Page):
        """Spec explorer includes actual spec source code."""
        resp = page.request.get(f"{BASE}/sx/(language.(spec.(explore.evaluator)))")
        body = resp.text()
        assert "define" in body, "Should contain define forms from spec"
        assert "eval-expr" in body, "Should contain eval-expr from evaluator spec"

    @pytest.mark.xfail(reason="Client re-evaluates defpage content; find-spec unavailable on client")
    def test_evaluator_renders_in_browser(self, page: Page):
        """Spec explorer should render correctly in the browser, not show 'not found'."""
        nav(page, "(language.(spec.(explore.evaluator)))")
        page.wait_for_timeout(3000)
        content = page.locator("#main-panel").text_content() or ""
        assert "not found" not in content.lower(), \
            f"Page shows 'not found' instead of spec content: {content[:200]}"
        expect(page.locator("#main-panel")).to_contain_text("Evaluator", timeout=5000)


# ---------------------------------------------------------------------------
# Key doc pages (smoke tests)
# ---------------------------------------------------------------------------

def _check_no_fatal_errors(errors):
    """Assert no fatal JS errors were collected."""
    fatal = [e for e in errors
             if any(s in e for s in ("Not callable", "Undefined symbol",
                                      "SES_UNCAUGHT", "UNCAUGHT"))]
    assert not fatal, f"JS errors: {fatal}"


def _setup_error_capture(page):
    """Attach error listeners, return the errors list."""
    errors = []
    page.on("pageerror", lambda err: errors.append(f"UNCAUGHT: {err.message}"))
    page.on("console", lambda msg: errors.append(msg.text) if msg.type == "error" else None)
    return errors


class TestHomePage:
    def test_home_has_header(self, page: Page):
        """Home page MUST have the (<sx>) header with logo."""
        errors = _setup_error_capture(page)
        nav(page, "")
        # Header logo — the (<sx>) text
        expect(page.locator("#logo-opacity")).to_be_visible(timeout=15000)
        _check_no_fatal_errors(errors)

    def test_home_has_nav_children(self, page: Page):
        """Home page MUST have nav children (Geography, Language, etc.)."""
        nav(page, "")
        expect(page.locator("a[sx-push-url]:has-text('Geography')")).to_be_visible(timeout=15000)

    def test_home_has_main_panel(self, page: Page):
        """Home page MUST render #main-panel with content inside it."""
        nav(page, "")
        expect(page.locator("#main-panel")).to_be_visible(timeout=15000)
        # Must have actual content (divs/headings), not be empty
        page.wait_for_selector("#main-panel div, #main-panel h2, #main-panel p", timeout=15000)

    def test_no_console_errors(self, page: Page):
        """Home page should have no JS errors (console or uncaught)."""
        errors = _setup_error_capture(page)
        page.goto(f"{BASE}/sx/", wait_until="networkidle")
        page.wait_for_timeout(3000)
        _check_no_fatal_errors(errors)

    def test_navigate_from_home_to_geography(self, page: Page):
        """Click Geography nav link from home — content must render."""
        errors = _setup_error_capture(page)
        nav(page, "")
        geo_link = page.locator("a[sx-push-url]:has-text('Geography')").first
        expect(geo_link).to_be_visible(timeout=10000)
        geo_link.click()
        page.wait_for_timeout(5000)
        expect(page.locator("#main-panel")).to_contain_text("Geography", timeout=10000)
        _check_no_fatal_errors(errors)

    def test_navigate_geography_to_reactive(self, page: Page):
        """Click Reactive Islands from Geography — content must render."""
        errors = _setup_error_capture(page)
        nav(page, "(geography)")
        ri_link = page.locator("a[sx-push-url]:has-text('Reactive Islands')").first
        expect(ri_link).to_be_visible(timeout=10000)
        ri_link.click()
        page.wait_for_timeout(5000)
        expect(page.locator("#main-panel")).to_contain_text("Reactive Islands", timeout=10000)
        _check_no_fatal_errors(errors)


class TestDirectPageLoad:
    """Direct page loads (not HTMX) — each uses a FRESH browser context
    to avoid stale component hash in localStorage from prior tests."""

    @pytest.mark.parametrize("path,expected_text", [
        ("(geography.(reactive))", "Reactive Islands"),
        ("(etc)", "Etc"),
        ("(geography)", "Geography"),
    ])
    def test_page_renders_with_header(self, browser, path: str, expected_text: str):
        """Page must have header, #main-panel, and expected content."""
        ctx = browser.new_context(ignore_https_errors=True)
        page = ctx.new_page()
        try:
            errors = _setup_error_capture(page)
            page.goto(f"{BASE}/sx/{path}", wait_until="networkidle")
            page.wait_for_selector("#main-panel", timeout=30000)
            expect(page.locator("#main-panel")).to_contain_text(expected_text, timeout=10000)
            expect(page.locator("#sx-nav")).to_be_visible(timeout=5000)
            _check_no_fatal_errors(errors)
        finally:
            ctx.close()


class TestDocPages:
    @pytest.mark.parametrize("path,expected", [
        ("(geography.(reactive))", "Reactive Islands"),
        ("(geography.(hypermedia.(reference.attributes)))", "Attributes"),
        ("(geography.(scopes))", "Scopes"),
        ("(geography.(provide))", "Provide"),
        ("(geography.(spreads))", "Spreads"),
        ("(language.(doc.introduction))", "Introduction"),
        ("(language.(spec.core))", "Core"),
        ("(applications.(cssx))", "CSSX"),
        ("(etc.(essay.why-sexps))", "Why S-Expressions"),
    ])
    def test_page_loads(self, page: Page, path: str, expected: str):
        nav(page, path)
        expect(page.locator("#main-panel")).to_contain_text(expected, timeout=10000)


# ---------------------------------------------------------------------------
# Navigation tests
# ---------------------------------------------------------------------------

class TestClientNavigation:
    def test_navigate_between_reactive_examples(self, page: Page):
        """Navigate from counter to temperature via server fetch."""
        nav(page, "(geography.(reactive.(examples.counter)))")
        expect(page.locator("#main-panel")).to_contain_text("signal holds a value", timeout=10000)

        # Click temperature link in sibling nav — has sx-get for server fetch
        temp_link = page.locator("a[sx-push-url]:has-text('Temperature')").first
        expect(temp_link).to_be_visible(timeout=5000)
        temp_link.click()
        page.wait_for_timeout(5000)
        expect(page.locator("#main-panel")).to_contain_text("Temperature", timeout=10000)

    def test_reactive_island_works_after_navigation(self, page: Page):
        """Island should be functional after client-side navigation."""
        nav(page, "(geography.(reactive.(examples.temperature)))")
        page.wait_for_timeout(3000)  # Wait for hydration

        # Click +5 and verify
        island = page.locator("[data-sx-island*='demo-temperature']")
        expect(island).to_be_visible(timeout=5000)
        island.locator("button", has_text="+5").click()
        page.wait_for_timeout(500)
        expect(island).to_contain_text("25")

    def test_full_refresh_preserves_content(self, page: Page):
        """Navigate to a page, then hard-refresh — content must re-render."""
        errors = _setup_error_capture(page)
        nav(page, "(geography.(hypermedia.(example.click-to-load)))")
        expect(page.locator("#main-panel")).to_contain_text("Click to Load")
        # Hard refresh
        page.reload(wait_until="networkidle")
        page.wait_for_timeout(2000)
        expect(page.locator("#main-panel")).to_contain_text("Click to Load", timeout=10000)
        _check_no_fatal_errors(errors)

    def test_back_button_after_navigation(self, page: Page):
        """Navigate A → B, then back — A must re-render without errors."""
        errors = _setup_error_capture(page)
        nav(page, "(geography.(reactive.(examples.counter)))")
        expect(page.locator("#main-panel")).to_contain_text("signal holds a value", timeout=10000)

        # Navigate to temperature
        temp_link = page.locator("a[sx-push-url]:has-text('Temperature')").first
        expect(temp_link).to_be_visible(timeout=5000)
        temp_link.click()
        page.wait_for_timeout(3000)
        expect(page.locator("#main-panel")).to_contain_text("Temperature", timeout=10000)

        # Go back
        page.go_back()
        page.wait_for_timeout(3000)
        expect(page.locator("#main-panel")).to_contain_text("signal holds a value", timeout=10000)
        _check_no_fatal_errors(errors)

    def test_direct_load_no_js_errors(self, browser):
        """Fresh browser context — direct page load must have zero JS errors."""
        ctx = browser.new_context(ignore_https_errors=True)
        page = ctx.new_page()
        try:
            errors = _setup_error_capture(page)
            page.goto(f"{BASE}/sx/(geography.(hypermedia.(example.delete-row)))", wait_until="networkidle")
            page.wait_for_timeout(3000)
            page.wait_for_selector("#main-panel", timeout=15000)
            _check_no_fatal_errors(errors)
        finally:
            ctx.close()
