"""
Step 24 — App Update Modal (no more silent freeze)

- test_modal_appears_when_update_pending: Modal appears when an update is pending
- test_later_dismisses_for_session: "Later" closes the modal and it stays closed across navigation
- test_update_now_shows_spinner_and_calls_update: "Update now" switches to the spinner state and calls the update function
- test_spinner_state_ignores_escape: The updating state cannot be dismissed with Escape
- test_spinner_state_has_no_close_button: The updating state has no header close button
- test_modal_suppressed_on_execution_route: Modal does not appear during session execution
- test_modal_appears_after_leaving_execution: Modal appears once the user navigates away from execution

The pending update is forced through the dev-only hook `window.__appUpdateTest`
(see src/composables/useAppUpdate.js) — simulating a real service-worker update
in Playwright would require two production builds. The real SW flow is
validated manually.
"""
from playwright.sync_api import Page, expect


def _wait_for_db(page: Page, app_url: str):
    page.goto(app_url)
    page.wait_for_function(
        "document.documentElement.getAttribute('data-db-ready') === 'true'",
        timeout=15000,
    )


def _trigger_update(page: Page):
    """Force the 'update available' state via the dev-only test hook."""
    page.wait_for_function("!!window.__appUpdateTest", timeout=10000)
    page.evaluate("window.__appUpdateTest.trigger()")


def _mock_update_fn(page: Page):
    """Replace the real SW update call with a recorder that never resolves
    (the real call ends in a page reload, which the spinner state covers)."""
    page.evaluate("""() => {
        window.__updateCalls = []
        window.__appUpdateTest.mockUpdateFn = (reload) => {
            window.__updateCalls.push(reload)
            return new Promise(() => {})
        }
    }""")


def _create_session(page: Page, title: str) -> str:
    """Create a one-exercise session and return its id."""
    return page.evaluate(f"""async () => {{
        const userId = await window.__repos.settings.get('user_uuid')
        const exId = await window.__repos.exercises.create({{
            title: {repr(title + ' exercise')},
            description: '',
            asymmetric: false,
            alternate: false,
            image_urls: '[]',
            video_urls: '[]',
            default_reps: 10,
            default_weight: 0,
            created_by: userId
        }})
        const sessId = await window.__repos.sessions.create({{
            title: {repr(title)}, description: '', created_by: userId
        }})
        await window.__repos.sessions.setItems(sessId, [{{
            item_id: exId, item_type: 'exercise',
            position: 0, repetitions: 1, weight: 0
        }}])
        return sessId
    }}""")


def test_modal_appears_when_update_pending(page: Page, app_url: str):
    """The update modal appears when an update is pending."""
    _wait_for_db(page, app_url)

    expect(page.locator('[data-testid="app-update-modal"]')).not_to_be_visible()
    _trigger_update(page)

    expect(page.locator('[data-testid="app-update-modal"]')).to_be_visible()
    expect(page.locator('[data-testid="app-update-now"]')).to_be_visible()
    expect(page.locator('[data-testid="app-update-later"]')).to_be_visible()


def test_later_dismisses_for_session(page: Page, app_url: str):
    """'Later' closes the modal and it does not reappear while navigating."""
    _wait_for_db(page, app_url)
    _trigger_update(page)

    modal = page.locator('[data-testid="app-update-modal"]')
    expect(modal).to_be_visible()

    page.locator('[data-testid="app-update-later"]').click()
    expect(modal).not_to_be_visible()

    # Navigating within the session does not bring the modal back
    page.evaluate("window.location.hash = '#/exercises'")
    page.wait_for_selector('[data-testid="exercise-new-btn"]', timeout=10000)
    expect(modal).not_to_be_visible()


def test_update_now_shows_spinner_and_calls_update(page: Page, app_url: str):
    """'Update now' switches to the spinner state and calls the update function."""
    _wait_for_db(page, app_url)
    _trigger_update(page)
    _mock_update_fn(page)

    page.locator('[data-testid="app-update-now"]').click()

    expect(page.locator('[data-testid="app-update-updating"]')).to_be_visible()
    # Choice buttons are gone
    expect(page.locator('[data-testid="app-update-now"]')).not_to_be_visible()
    expect(page.locator('[data-testid="app-update-later"]')).not_to_be_visible()

    calls = page.evaluate("window.__updateCalls")
    assert calls == [True], f"updateServiceWorker should be called with reload=true, got {calls}"


def test_spinner_state_ignores_escape(page: Page, app_url: str):
    """The updating state cannot be dismissed with Escape or a backdrop click."""
    _wait_for_db(page, app_url)
    _trigger_update(page)
    _mock_update_fn(page)

    page.locator('[data-testid="app-update-now"]').click()
    updating = page.locator('[data-testid="app-update-updating"]')
    expect(updating).to_be_visible()

    page.keyboard.press("Escape")
    expect(updating).to_be_visible()

    # Backdrop click must not close it either
    page.locator('[data-testid="modal-overlay"]').click(position={"x": 5, "y": 5})
    expect(updating).to_be_visible()


def test_spinner_state_has_no_close_button(page: Page, app_url: str):
    """The updating state renders no header close (×) button."""
    _wait_for_db(page, app_url)
    _trigger_update(page)
    _mock_update_fn(page)

    expect(page.locator('[data-testid="modal-close"]')).to_be_visible()
    page.locator('[data-testid="app-update-now"]').click()

    expect(page.locator('[data-testid="app-update-updating"]')).to_be_visible()
    expect(page.locator('[data-testid="modal-close"]')).not_to_be_visible()


def test_modal_suppressed_on_execution_route(page: Page, app_url: str):
    """The modal does not appear while a session is being executed."""
    _wait_for_db(page, app_url)
    sess_id = _create_session(page, "Update modal session")

    page.evaluate(f"window.location.hash = '#/sessions/{sess_id}/execute'")
    page.wait_for_selector('[data-testid="execute-session-title"]', timeout=10000)

    _trigger_update(page)
    expect(page.locator('[data-testid="app-update-modal"]')).not_to_be_visible()


def test_modal_appears_after_leaving_execution(page: Page, app_url: str):
    """The pending modal appears once the user navigates away from execution."""
    _wait_for_db(page, app_url)
    sess_id = _create_session(page, "Update modal deferred")

    page.evaluate(f"window.location.hash = '#/sessions/{sess_id}/execute'")
    page.wait_for_selector('[data-testid="execute-session-title"]', timeout=10000)

    _trigger_update(page)
    expect(page.locator('[data-testid="app-update-modal"]')).not_to_be_visible()

    # Leave execution (accept the abandon-confirmation dialog)
    page.on("dialog", lambda dialog: dialog.accept())
    page.evaluate("window.location.hash = '#/home'")

    expect(page.locator('[data-testid="app-update-modal"]')).to_be_visible()
