"""
Step 3c — Backup Filename & Reminder Tests
- test_download_uses_new_filename: Download uses the new backup filename pattern
- test_download_sets_last_backup_at: After download, last_backup_at is set in DB
- test_reminder_hidden_when_fresh: Reminder is hidden when backup is recent
- test_reminder_visible_when_stale: Reminder appears when backup is older than threshold
- test_reminder_visible_when_never: Reminder appears when no backup has been done and db_created_at is old
- test_reminder_hidden_when_db_fresh_no_backup: Reminder hidden when no backup but db_created_at is recent
- test_reminder_click_navigates_to_settings: Clicking reminder navigates to settings page
- test_restore_updates_last_backup_at: Restore updates last_backup_at timestamp
- test_custom_threshold_is_respected: Custom reminder days threshold is respected
- test_invalid_threshold_rejected: Invalid threshold values are rejected
- test_filename_is_filesystem_safe: Filename contains no colons and is zero-padded
- test_startup_modal_appears_when_stale: Startup modal appears on load when backup is overdue
- test_startup_modal_dismiss: Clicking Dismiss closes the startup modal
- test_startup_modal_go_to_settings: Clicking Go to Settings navigates to settings page
- test_startup_modal_does_not_reappear: Modal does not reappear when navigating after dismissal
"""
import re
from datetime import datetime, timedelta, timezone
from playwright.sync_api import Page, expect


def _iso(days_ago: int) -> str:
    """Return an ISO-8601 UTC timestamp for N days in the past."""
    dt = datetime.now(timezone.utc) - timedelta(days=days_ago)
    return dt.strftime("%Y-%m-%dT%H:%M:%S.000Z")


def _wait_for_db(page: Page, app_url: str):
    """Navigate and wait for the database to be ready."""
    page.goto(app_url)
    page.wait_for_function(
        "document.documentElement.getAttribute('data-db-ready') === 'true'",
        timeout=15000,
    )


def _go_to_settings(page: Page, app_url: str):
    """Navigate to the settings page and wait for DB."""
    _wait_for_db(page, app_url)
    page.click('a[href="#/settings"]')
    page.wait_for_selector('[data-testid="settings-language"]')


def _download_via_button(page: Page, trigger_selector: str):
    """Click a button that opens the save-filename dialog, accept the default name, return the Download."""
    page.locator(trigger_selector).click()
    page.wait_for_selector('[data-testid="save-file-confirm"]')
    with page.expect_download() as download_info:
        page.locator('[data-testid="save-file-confirm"]').click()
    return download_info.value


def test_download_uses_new_filename(page: Page, app_url: str):
    """Download uses the new backup filename pattern with version and timestamp."""
    _go_to_settings(page, app_url)

    download = _download_via_button(page, '[data-testid="settings-download-db"]')
    filename = download.suggested_filename

    # Match pattern: trainus-{version}-backup-YYYYMMDD-HHhMMmSSs.sqlite3
    pattern = r'^trainus-\d+\.\d+\.\d+-backup-\d{8}-\d{2}h\d{2}m\d{2}s\.sqlite3$'
    assert re.match(pattern, filename), f"Filename '{filename}' does not match expected pattern"


def test_download_sets_last_backup_at(page: Page, app_url: str):
    """After download, last_backup_at is set and Settings shows the backup date."""
    _go_to_settings(page, app_url)

    # Click download
    _download_via_button(page, '[data-testid="settings-download-db"]')

    # Wait for the backup status to update
    page.wait_for_timeout(1000)

    # Check that last_backup_at is set in DB
    last_backup = page.evaluate("async () => await window.__repos.settings.get('last_backup_at')")
    assert last_backup is not None, "last_backup_at should be set after download"

    # Settings should show the backup date
    backup_info = page.locator('[data-testid="backup-last-backup"]')
    expect(backup_info).to_be_visible()
    text = backup_info.text_content()
    assert 'Never backed up' not in text, "Should show backup date, not 'Never backed up'"


def test_reminder_hidden_when_fresh(page: Page, app_url: str):
    """Reminder is hidden when backup is recent (within threshold)."""
    _go_to_settings(page, app_url)

    # Do a download to create a fresh backup
    _download_via_button(page, '[data-testid="settings-download-db"]')

    page.wait_for_timeout(1000)

    # Navigate away and back to check reminder visibility
    page.click('a[href="#/home"]')
    page.wait_for_function(
        "document.documentElement.getAttribute('data-db-ready') === 'true'",
        timeout=15000,
    )

    # Reminder should not be visible
    reminder = page.locator('[data-testid="backup-reminder"]')
    expect(reminder).not_to_be_visible()


def test_reminder_visible_when_stale(page: Page, app_url: str):
    """Reminder appears when backup is older than the threshold."""
    _go_to_settings(page, app_url)

    # Set last_backup_at to 8 days ago (default threshold is 7)
    page.evaluate(f"window.__repos.settings.set('last_backup_at', '{_iso(8)}')")

    # Go to home page to check reminder
    page.click('a[href="#/home"]')
    page.wait_for_function(
        "document.documentElement.getAttribute('data-db-ready') === 'true'",
        timeout=15000,
    )

    # Reminder should be visible
    reminder = page.locator('[data-testid="backup-reminder"]')
    expect(reminder).to_be_visible()


def test_reminder_visible_when_never(page: Page, app_url: str):
    """Reminder appears when no backup has been done and db_created_at is old."""
    page.goto(app_url)
    page.wait_for_function(
        "document.documentElement.getAttribute('data-db-ready') === 'true'",
        timeout=15000,
    )

    # Remove both last_backup_at AND set db_created_at to an old date
    # so the fallback makes the reminder appear
    page.evaluate("async () => { await window.__repos.settings.set('last_backup_at', null) }")
    page.evaluate(f"async () => {{ await window.__repos.settings.set('db_created_at', '{_iso(30)}') }}")

    # Reload to pick up the changed settings
    page.reload()
    page.wait_for_function(
        "document.documentElement.getAttribute('data-db-ready') === 'true'",
        timeout=15000,
    )

    # Reminder should be visible
    reminder = page.locator('[data-testid="backup-reminder"]')
    expect(reminder).to_be_visible()

    # Restore db_created_at to a recent value so subsequent tests on the same
    # browser page (same OPFS database) are not affected by the stale date.
    page.evaluate(f"async () => {{ await window.__repos.settings.set('db_created_at', '{_iso(0)}') }}")
    page.reload()
    page.wait_for_function(
        "document.documentElement.getAttribute('data-db-ready') === 'true'",
        timeout=15000,
    )


def test_reminder_hidden_when_db_fresh_no_backup(page: Page, app_url: str):
    """Reminder is hidden when no backup exists but db_created_at is recent."""
    _wait_for_db(page, app_url)

    # Clear only last_backup_at, leaving db_created_at intact (recent)
    page.evaluate("async () => { await window.__repos.settings.set('last_backup_at', null) }")

    # Reload to pick up the changed settings
    page.reload()
    page.wait_for_function(
        "document.documentElement.getAttribute('data-db-ready') === 'true'",
        timeout=15000,
    )

    # Reminder should NOT be visible (db_created_at is recent)
    reminder = page.locator('[data-testid="backup-reminder"]')
    expect(reminder).not_to_be_visible()


def test_reminder_click_navigates_to_settings(page: Page, app_url: str):
    """Clicking reminder navigates to the settings page."""
    _go_to_settings(page, app_url)

    # Set a stale backup date
    page.evaluate(f"window.__repos.settings.set('last_backup_at', '{_iso(8)}')")

    # Go to home page
    page.click('a[href="#/home"]')
    page.wait_for_timeout(1000)

    # Click the reminder
    page.locator('[data-testid="backup-reminder"]').click()

    # Should navigate to settings
    page.wait_for_url(re.compile(r'#/settings'), timeout=5000)

    # Backup section should be visible
    backup_section = page.locator('#settings-backup-section')
    expect(backup_section).to_be_visible()


def test_restore_updates_last_backup_at(page: Page, app_url: str, tmp_path_factory):
    """Successful restore updates last_backup_at timestamp."""
    _go_to_settings(page, app_url)

    # Download DB and save to a known path (download.path() can be None under xdist)
    tmp_dir = tmp_path_factory.mktemp("restore")
    db_path = str(tmp_dir / "backup.sqlite3")
    download = _download_via_button(page, '[data-testid="settings-download-db"]')
    download.save_as(db_path)

    # Wait for the download's markBackedUp() to persist, then clear last_backup_at
    page.wait_for_timeout(500)
    page.evaluate("async () => { await window.__repos.settings.set('last_backup_at', null) }")
    # Verify it is actually null before proceeding
    val = page.evaluate("async () => await window.__repos.settings.get('last_backup_at')")
    assert val is None, f"last_backup_at should be null before restore, got: {val}"

    # Restore the DB — use once() to avoid handler accumulation across tests
    page.once('dialog', lambda dialog: dialog.accept())
    with page.expect_file_chooser() as fc_info:
        page.locator('[data-testid="settings-restore-db"]').click()
    file_chooser = fc_info.value
    file_chooser.set_files(db_path)

    # Wait for the spinner to disappear — indicates the full async chain completed
    page.wait_for_selector('[data-testid="restore-spinner"]', state="hidden", timeout=15000)

    # last_backup_at should be set by markBackedUp() after successful restore
    last_backup = page.evaluate("async () => await window.__repos.settings.get('last_backup_at')")
    assert last_backup is not None, "last_backup_at should be set after restore"


def test_custom_threshold_is_respected(page: Page, app_url: str):
    """Custom reminder days threshold is respected."""
    _go_to_settings(page, app_url)

    # Set last_backup_at to 3 days ago
    page.evaluate(f"window.__repos.settings.set('last_backup_at', '{_iso(3)}')")

    # Set threshold to 2 days
    page.locator('[data-testid="backup-reminder-days"]').fill('2')
    page.locator('[data-testid="backup-reminder-days"]').blur()
    page.wait_for_timeout(1000)

    # Reload the page to pick up new threshold
    page.reload()
    page.wait_for_function(
        "document.documentElement.getAttribute('data-db-ready') === 'true'",
        timeout=15000,
    )

    # Reminder should be visible (3 days > 2 day threshold)
    reminder = page.locator('[data-testid="backup-reminder"]')
    expect(reminder).to_be_visible()

    # The startup modal appears on reload — dismiss it before navigating
    page.locator('[data-testid="backup-modal-dismiss"]').click()

    # Go back to settings and change threshold to 30 days
    page.click('a[href="#/settings"]')
    page.wait_for_selector('[data-testid="settings-language"]')

    page.locator('[data-testid="backup-reminder-days"]').fill('30')
    page.locator('[data-testid="backup-reminder-days"]').blur()
    page.wait_for_timeout(1000)

    # Reload the page to pick up new threshold
    page.reload()
    page.wait_for_function(
        "document.documentElement.getAttribute('data-db-ready') === 'true'",
        timeout=15000,
    )

    # Reminder should be hidden (3 days < 30 day threshold)
    expect(reminder).not_to_be_visible()


def test_invalid_threshold_rejected(page: Page, app_url: str):
    """Invalid threshold values (0, -1, abc, 366) are rejected."""
    _go_to_settings(page, app_url)

    # Set a known valid value first
    page.locator('[data-testid="backup-reminder-days"]').fill('7')
    page.locator('[data-testid="backup-reminder-days"]').blur()
    page.wait_for_timeout(500)

    # Test invalid values
    invalid_values = ['0', '-1', 'abc', '366']
    for value in invalid_values:
        page.locator('[data-testid="backup-reminder-days"]').fill(value)
        page.locator('[data-testid="backup-reminder-days"]').blur()
        page.wait_for_timeout(500)

        # Error should be visible
        error = page.locator('[data-testid="backup-reminder-error"]')
        expect(error).to_be_visible()

    # DB should still have the last valid value (7)
    stored = page.evaluate("async () => await window.__repos.settings.get('backup_reminder_days')")
    assert stored == '7', f"backup_reminder_days should be '7', got '{stored}'"


def test_filename_is_filesystem_safe(page: Page, app_url: str):
    """Filename contains no colons and is zero-padded."""
    _go_to_settings(page, app_url)

    download = _download_via_button(page, '[data-testid="settings-download-db"]')
    filename = download.suggested_filename

    # No colons
    assert ':' not in filename, f"Filename '{filename}' contains colons"

    # Zero-padded: check the timestamp portion
    # Extract the timestamp part: YYYYMMDD-HHhMMmSSs
    match = re.search(r'backup-(\d{8}-\d{2}h\d{2}m\d{2}s)', filename)
    assert match, f"Filename '{filename}' does not have zero-padded timestamp"


def _set_stale_and_reload(page: Page, app_url: str):
    """Set db_created_at to 30 days ago (no last_backup_at) then reload."""
    page.goto(app_url)
    page.wait_for_function(
        "document.documentElement.getAttribute('data-db-ready') === 'true'",
        timeout=15000,
    )
    page.evaluate("async () => { await window.__repos.settings.set('last_backup_at', null) }")
    page.evaluate(f"async () => {{ await window.__repos.settings.set('db_created_at', '{_iso(30)}') }}")
    page.reload()
    page.wait_for_function(
        "document.documentElement.getAttribute('data-db-ready') === 'true'",
        timeout=15000,
    )


def _cleanup_stale(page: Page):
    """Reset db_created_at to today so the stale state does not leak."""
    page.evaluate(f"async () => {{ await window.__repos.settings.set('db_created_at', '{_iso(0)}') }}")


def test_startup_modal_appears_when_stale(page: Page, app_url: str):
    """Startup modal appears on page load when backup is overdue."""
    _set_stale_and_reload(page, app_url)

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

    _cleanup_stale(page)


def test_startup_modal_dismiss(page: Page, app_url: str):
    """Clicking Dismiss closes the startup modal."""
    _set_stale_and_reload(page, app_url)

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

    page.locator('[data-testid="backup-modal-dismiss"]').click()
    expect(modal).not_to_be_visible()

    _cleanup_stale(page)


def test_startup_modal_go_to_settings(page: Page, app_url: str):
    """Clicking Go to Settings in the startup modal navigates to the settings page and closes the modal."""
    _set_stale_and_reload(page, app_url)

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

    page.locator('[data-testid="backup-modal-settings"]').click()

    expect(modal).not_to_be_visible()
    page.wait_for_url(re.compile(r'#/settings'), timeout=5000)

    _cleanup_stale(page)


def test_startup_modal_does_not_reappear(page: Page, app_url: str):
    """After dismissal the startup modal does not reappear when navigating within the same session."""
    _set_stale_and_reload(page, app_url)

    page.locator('[data-testid="backup-modal-dismiss"]').click()

    modal = page.locator('[data-testid="backup-modal"]')

    # Navigate away and back — component remounts each time due to :key binding
    page.click('a[href="#/exercises"]')
    page.wait_for_timeout(500)
    page.click('a[href="#/home"]')
    page.wait_for_timeout(500)

    expect(modal).not_to_be_visible()

    _cleanup_stale(page)
