"""
Step 18 — Execution Abandon Tests (R4)

R4 — Execution log status column + route-leave guard:
- test_completed_session_has_completed_status: Finishing a session sets status='completed'
- test_exit_button_sets_aborted_status: Using the exit button sets status='aborted'
- test_leave_guard_shows_confirm_dialog: Navigating away mid-session triggers a confirm dialog
- test_leave_guard_cancel_stays_on_page: Cancelling the confirm keeps the user on the execute view
- test_leave_guard_confirm_sets_aborted: Confirming the leave dialog marks the log as aborted
- test_no_null_finished_at_after_abandon: After any navigation away, no log has finished_at=NULL
- test_stats_only_count_completed_sessions: Stats total-sessions count excludes aborted logs
"""
import json
import pytest
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 _clean_all(page: Page):
    page.evaluate("""async () => {
        const logs = await window.__repos.executionLogs.getAll()
        for (const l of logs) await window.__repos.executionLogs.delete(l.id)
        const sessions = await window.__repos.sessions.getAll()
        for (const s of sessions) await window.__repos.sessions.delete(s.id)
        const exercises = await window.__repos.exercises.getAll()
        for (const e of exercises) await window.__repos.exercises.delete(e.id)
    }""")


def _create_exercise(page: Page, title: str) -> str:
    return page.evaluate(
        """async ([title]) => {
            const userId = await window.__repos.settings.get('user_uuid')
            return window.__repos.exercises.create({
                title, description: '', asymmetric: false, alternate: false,
                image_urls: '[]', video_urls: '[]', default_reps: 5, default_weight: 0,
                created_by: userId
            })
        }""",
        [title],
    )


def _create_session(page: Page, title: str, ex_id: str) -> str:
    return page.evaluate(
        """async ([title, exId]) => {
            const userId = await window.__repos.settings.get('user_uuid')
            const sessId = await window.__repos.sessions.create({title, description: '', created_by: userId})
            await window.__repos.sessions.setItems(sessId, [{
                item_id: exId, item_type: 'exercise', position: 0, repetitions: 5, weight: 0
            }])
            return sessId
        }""",
        [title, ex_id],
    )


def _navigate_to_execute(page: Page, app_url: str, sess_id: str):
    """Navigate to the execute view for a given session."""
    page.goto(f"{app_url}#/sessions/{sess_id}/execute")
    page.wait_for_selector('[data-testid="execute-session-title"]', timeout=10000)
    page.wait_for_timeout(300)


@pytest.fixture
def session_and_exercise(page: Page, app_url: str):
    """Create a session with one exercise and return (sess_id, ex_id)."""
    _wait_for_db(page, app_url)
    _clean_all(page)
    ex_id = _create_exercise(page, "Test Exercise")
    sess_id = _create_session(page, "Test Session", ex_id)
    return sess_id, ex_id


def test_completed_session_has_completed_status(page: Page, app_url: str, session_and_exercise):
    """Finishing all steps sets the execution log status to 'completed'."""
    sess_id, _ = session_and_exercise
    _navigate_to_execute(page, app_url, sess_id)

    # Click Done to complete the single exercise step — for a one-exercise session this
    # redirects directly to the session detail view (no final-round screen)
    page.click('[data-testid="execute-done-btn"]')
    page.wait_for_function(
        f"window.location.hash.includes('/sessions/{sess_id}') && !window.location.hash.includes('/execute')",
        timeout=5000,
    )
    page.wait_for_timeout(300)

    logs = page.evaluate("""async () => window.__repos.executionLogs.getAll()""")
    assert len(logs) == 1, f"Expected 1 log, got {len(logs)}"
    assert logs[0]["status"] == "completed", f"Expected status=completed, got: {logs[0]}"
    assert logs[0]["finished_at"] is not None, "finished_at should be set"


def test_exit_button_sets_aborted_status(page: Page, app_url: str, session_and_exercise):
    """Using the exit button during execution marks the log status as 'aborted'."""
    sess_id, _ = session_and_exercise
    _navigate_to_execute(page, app_url, sess_id)

    # Click the exit button and confirm the dialog
    page.once("dialog", lambda d: d.accept())
    page.click('[data-testid="execute-exit-btn"]')
    page.wait_for_timeout(500)

    logs = page.evaluate("""async () => window.__repos.executionLogs.getAll()""")
    assert len(logs) == 1, f"Expected 1 log, got {len(logs)}"
    assert logs[0]["status"] == "aborted", f"Expected status=aborted, got: {logs[0]}"
    assert logs[0]["finished_at"] is not None, "finished_at should be set on abort"


def test_leave_guard_shows_confirm_dialog(page: Page, app_url: str, session_and_exercise):
    """Navigating away via the nav bar while execution is in progress triggers a confirm dialog."""
    sess_id, _ = session_and_exercise
    _navigate_to_execute(page, app_url, sess_id)

    # Intercept the dialog to detect it appeared, then dismiss (cancel)
    dialog_appeared = []
    page.once("dialog", lambda d: (dialog_appeared.append(d.message), d.dismiss()))
    page.click('a[href="#/home"]')
    page.wait_for_timeout(500)

    assert len(dialog_appeared) > 0, "Leave guard should show a confirm dialog"
    # Should still be on the execute view (dismiss = stay)
    page.wait_for_selector('[data-testid="execute-session-title"]', timeout=3000)


def test_leave_guard_cancel_stays_on_page(page: Page, app_url: str, session_and_exercise):
    """Dismissing the leave confirm keeps the user on the execute view."""
    sess_id, _ = session_and_exercise
    _navigate_to_execute(page, app_url, sess_id)

    page.once("dialog", lambda d: d.dismiss())
    page.click('a[href="#/home"]')
    page.wait_for_timeout(500)

    title_el = page.locator('[data-testid="execute-session-title"]')
    expect(title_el).to_be_visible()


def test_leave_guard_confirm_sets_aborted(page: Page, app_url: str, session_and_exercise):
    """Accepting the leave confirm marks the log as aborted and navigates away."""
    sess_id, _ = session_and_exercise
    _navigate_to_execute(page, app_url, sess_id)

    # Accept the dialog → exit() is called → status='aborted'
    page.once("dialog", lambda d: d.accept())
    page.click('a[href="#/home"]')
    page.wait_for_timeout(800)

    # We should no longer be on the execute view
    title_el = page.locator('[data-testid="execute-session-title"]')
    expect(title_el).not_to_be_visible(timeout=3000)

    logs = page.evaluate("""async () => window.__repos.executionLogs.getAll()""")
    assert len(logs) == 1, f"Expected 1 log after abandon, got {len(logs)}"
    assert logs[0]["status"] == "aborted", f"Expected aborted, got: {logs[0]['status']}"


def test_no_null_finished_at_after_abandon(page: Page, app_url: str, session_and_exercise):
    """After confirming the leave dialog, no execution_log row has finished_at=NULL."""
    sess_id, _ = session_and_exercise
    _navigate_to_execute(page, app_url, sess_id)

    page.once("dialog", lambda d: d.accept())
    page.click('a[href="#/home"]')
    page.wait_for_timeout(800)

    null_logs = page.evaluate(
        """async () => {
            const logs = await window.__repos.executionLogs.getAll()
            return logs.filter(l => l.finished_at === null || l.finished_at === undefined)
        }"""
    )
    assert len(null_logs) == 0, f"No log should have NULL finished_at; orphans: {null_logs}"


def test_stats_only_count_completed_sessions(page: Page, app_url: str):
    """Stats total-sessions card counts only completed logs, not aborted ones."""
    _wait_for_db(page, app_url)
    _clean_all(page)

    ex_id = _create_exercise(page, "Stat Exercise")
    sess_id = _create_session(page, "Stat Session", ex_id)

    now = "2026-01-15T10:00:00.000Z"

    # Seed one completed log and one aborted log
    page.evaluate(
        """async ([sessId, exId, now]) => {
            const completedId = await window.__repos.executionLogs.create({
                session_id: sessId,
                started_at: now,
                finished_at: now,
            })
            await window.__repos.executionLogs.update(completedId, { status: 'completed', finished_at: now })

            const abortedId = await window.__repos.executionLogs.create({
                session_id: sessId,
                started_at: now,
                finished_at: now,
            })
            await window.__repos.executionLogs.update(abortedId, { status: 'aborted', finished_at: now })
        }""",
        [sess_id, ex_id, now],
    )

    # Navigate to stats (all-time range)
    page.evaluate("() => { window.location.hash = '#/stats' }")
    page.wait_for_selector('[data-testid="stats-total-sessions"]', timeout=5000)
    page.wait_for_timeout(500)

    # Select all-time tab (last tab)
    date_range = page.locator('[data-testid="stats-date-range"] button')
    count = date_range.count()
    date_range.nth(count - 1).click()
    page.wait_for_timeout(300)

    total_text = page.locator('[data-testid="stats-total-sessions"]').text_content()
    # The count should be 1 (only completed), not 2
    assert "1" in total_text, f"Stats should count 1 completed session, got text: '{total_text}'"
