"""
Step 8 — Execution Mode Tests

- test_start_session_navigates_to_execute: Clicking play from detail view navigates to execute view
- test_session_list_play_buttons: Play buttons appear on each session card in the list
- test_execute_view_elements: Execute view shows title, progress, current step, next step
- test_steps_advance_on_done: Pressing Done advances to the next step
- test_reps_weight_editable_and_saved: Reps and weight inputs are editable and saved to log
- test_session_complete_after_last_step: Completing all steps redirects to session detail
- test_asymmetric_side_shown: Side toggle visible for asymmetric exercises only
- test_asymmetric_side_logged: Side value is saved in the execution log
- test_combo_rounds_auto_advance: Non-final combo rounds advance silently (no pause screen)
- test_final_round_screen_shown: Final-round screen appears after last exercise of last round
- test_continue_advances_past_combo: Continue button advances to next session item
- test_one_more_round_appends_round: One More Round inserts an extra round into the queue
- test_exit_mid_session_saves_partial_log: Exit saves only completed items to log
- test_prefill_from_last_session_run: Re-executing a session prefills from last run
- test_no_prior_log_uses_defaults: No prior log uses session item defaults for prefill
- test_progress_bar_reflects_position: Progress label updates as steps are completed
- test_combo_context_shown: Combo name and round number shown in step card for combo exercises
"""
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 _wait_for_execute(page: Page):
    """Wait for the execute view to be fully loaded."""
    page.wait_for_selector('[data-testid="execute-session-title"]', timeout=10000)
    page.wait_for_timeout(300)


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


def _create_session_with_exercises(page: Page, title: str, items: list):
    """items: list of dicts with exerciseId, reps, weight."""
    items_json = str(items).replace("'", '"').replace("True", "true").replace("False", "false")
    return page.evaluate(f"""async () => {{
        const userId = await window.__repos.settings.get('user_uuid')
        const sessId = await window.__repos.sessions.create({{
            title: {repr(title)}, description: '', created_by: userId
        }})
        const items = {items_json}
        const mapped = items.map((it, i) => ({{
            item_id: it.item_id, item_type: it.item_type,
            position: i, repetitions: it.repetitions, weight: it.weight
        }}))
        await window.__repos.sessions.setItems(sessId, mapped)
        return sessId
    }}""")


def _create_combo(page: Page, title: str, exercises: list):
    """exercises: list of {{exerciseId, defaultReps, defaultWeight}}."""
    exercises_json = str(exercises).replace("'", '"').replace("True", "true").replace("False", "false")
    return page.evaluate(f"""async () => {{
        const userId = await window.__repos.settings.get('user_uuid')
        const comboId = await window.__repos.combos.create({{
            title: {repr(title)}, description: '', type: 'NONE',
            timer_config: {{}}, created_by: userId
        }})
        const exercises = {exercises_json}
        await window.__repos.combos.setExercises(comboId, exercises.map(e => ({{
            exerciseId: e.exerciseId,
            defaultReps: e.defaultReps,
            defaultWeight: e.defaultWeight
        }})))
        return comboId
    }}""")


def _clean_all(page: Page):
    page.evaluate("""async () => {
        const sessions = await window.__repos.sessions.getAll()
        for (const s of sessions) await window.__repos.sessions.delete(s.id)
        const combos = await window.__repos.combos.getAll()
        for (const c of combos) await window.__repos.combos.delete(c.id)
        const exercises = await window.__repos.exercises.getAll()
        for (const e of exercises) await window.__repos.exercises.delete(e.id)
    }""")


# ── fixtures ──────────────────────────────────────────────────────────────


@pytest.fixture
def simple_session(page: Page, app_url: str):
    """A session with one exercise."""
    _wait_for_db(page, app_url)
    _clean_all(page)
    ex_id = _create_exercise(page, "Press", default_reps=8, default_weight=50)
    sess_id = _create_session_with_exercises(page, "Simple Session", [
        {"item_id": ex_id, "item_type": "exercise", "repetitions": 8, "weight": 50}
    ])
    return {"sess_id": sess_id, "ex_id": ex_id}


@pytest.fixture
def two_step_session(page: Page, app_url: str):
    """A session with two exercises."""
    _wait_for_db(page, app_url)
    _clean_all(page)
    ex1 = _create_exercise(page, "Step One", default_reps=5, default_weight=30)
    ex2 = _create_exercise(page, "Step Two", default_reps=10, default_weight=0)
    sess_id = _create_session_with_exercises(page, "Two Step Session", [
        {"item_id": ex1, "item_type": "exercise", "repetitions": 5, "weight": 30},
        {"item_id": ex2, "item_type": "exercise", "repetitions": 10, "weight": 0},
    ])
    return {"sess_id": sess_id, "ex1": ex1, "ex2": ex2}


@pytest.fixture
def asymmetric_session(page: Page, app_url: str):
    """A session with an asymmetric exercise."""
    _wait_for_db(page, app_url)
    _clean_all(page)
    ex_id = _create_exercise(page, "Lunge", asymmetric=True, alternate=True,
                             default_reps=12, default_weight=0)
    sess_id = _create_session_with_exercises(page, "Asym Session", [
        {"item_id": ex_id, "item_type": "exercise", "repetitions": 12, "weight": 0}
    ])
    return {"sess_id": sess_id, "ex_id": ex_id}


@pytest.fixture
def combo_session(page: Page, app_url: str):
    """A session with a 2-exercise combo run for 2 rounds."""
    _wait_for_db(page, app_url)
    _clean_all(page)
    ex1 = _create_exercise(page, "Squat", default_reps=15, default_weight=0)
    ex2 = _create_exercise(page, "Jump", default_reps=20, default_weight=0)
    combo_id = _create_combo(page, "HIIT", [
        {"exerciseId": ex1, "defaultReps": 15, "defaultWeight": 0},
        {"exerciseId": ex2, "defaultReps": 20, "defaultWeight": 0},
    ])
    sess_id = _create_session_with_exercises(page, "Combo Session", [
        {"item_id": combo_id, "item_type": "combo", "repetitions": 2, "weight": 0}
    ])
    return {"sess_id": sess_id, "combo_id": combo_id, "ex1": ex1, "ex2": ex2}


@pytest.fixture
def mixed_session(page: Page, app_url: str):
    """A session with an exercise followed by a combo."""
    _wait_for_db(page, app_url)
    _clean_all(page)
    ex_solo = _create_exercise(page, "Solo Ex", default_reps=5, default_weight=0)
    ex_c1 = _create_exercise(page, "Combo Ex1", default_reps=10, default_weight=0)
    ex_c2 = _create_exercise(page, "Combo Ex2", default_reps=10, default_weight=0)
    combo_id = _create_combo(page, "Mini Combo", [
        {"exerciseId": ex_c1, "defaultReps": 10, "defaultWeight": 0},
        {"exerciseId": ex_c2, "defaultReps": 10, "defaultWeight": 0},
    ])
    sess_id = _create_session_with_exercises(page, "Mixed Session", [
        {"item_id": ex_solo, "item_type": "exercise", "repetitions": 5, "weight": 0},
        {"item_id": combo_id, "item_type": "combo", "repetitions": 1, "weight": 0},
    ])
    return {"sess_id": sess_id}


# ── tests ─────────────────────────────────────────────────────────────────


def test_start_session_navigates_to_execute(page: Page, app_url: str, simple_session):
    """Clicking play button from the session detail view navigates to execute view."""
    sess_id = simple_session["sess_id"]
    page.goto(f"{app_url}#/sessions/{sess_id}")
    page.wait_for_timeout(600)

    execute_btn = page.locator('[data-testid="session-execute-btn"]')
    expect(execute_btn).to_be_visible()
    execute_btn.click()
    page.wait_for_timeout(800)

    assert f"/sessions/{sess_id}/execute" in page.url
    expect(page.locator('[data-testid="execute-session-title"]')).to_be_visible()


def test_session_list_play_buttons(page: Page, app_url: str, simple_session):
    """Every session card in the list shows a play button."""
    sess_id = simple_session["sess_id"]
    page.goto(f"{app_url}#/sessions")
    page.wait_for_timeout(600)

    play_btn = page.locator(f'[data-testid="session-card-play-{sess_id}"]')
    expect(play_btn).to_be_visible()


def test_execute_view_elements(page: Page, app_url: str, two_step_session):
    """Execute view shows session title, progress, current step, and next step preview."""
    sess_id = two_step_session["sess_id"]
    page.goto(f"{app_url}#/sessions/{sess_id}/execute")
    _wait_for_execute(page)

    expect(page.locator('[data-testid="execute-session-title"]')).to_have_text("Two Step Session")
    expect(page.locator('[data-testid="execute-progress-label"]')).to_contain_text("0 / 2")
    expect(page.locator('[data-testid="execute-current-step"]')).to_be_visible()
    expect(page.locator('[data-testid="execute-reps-input"]')).to_be_visible()
    expect(page.locator('[data-testid="execute-weight-input"]')).to_be_visible()
    expect(page.locator('[data-testid="execute-done-btn"]')).to_be_visible()
    expect(page.locator('[data-testid="execute-next-step"]')).to_be_visible()


def test_steps_advance_on_done(page: Page, app_url: str, two_step_session):
    """Pressing Done advances from one step to the next."""
    sess_id = two_step_session["sess_id"]
    page.goto(f"{app_url}#/sessions/{sess_id}/execute")
    _wait_for_execute(page)

    first_title = page.locator('[data-testid="execute-exercise-title"]').inner_text()
    assert first_title == "Step One"

    page.click('[data-testid="execute-done-btn"]')
    page.wait_for_timeout(500)

    second_title = page.locator('[data-testid="execute-exercise-title"]').inner_text()
    assert second_title == "Step Two"

    progress = page.locator('[data-testid="execute-progress-label"]').inner_text()
    assert "1 / 2" in progress


def test_reps_weight_editable_and_saved(page: Page, app_url: str, simple_session):
    """Reps and weight inputs can be edited and the values are saved to the execution log."""
    sess_id = simple_session["sess_id"]
    page.goto(f"{app_url}#/sessions/{sess_id}/execute")
    _wait_for_execute(page)

    page.fill('[data-testid="execute-reps-input"]', "12")
    page.fill('[data-testid="execute-weight-input"]', "55")
    page.click('[data-testid="execute-done-btn"]')
    page.wait_for_timeout(800)

    # Check log saved correct values
    result = page.evaluate(f"""async () => {{
        const logs = await window.__repos.executionLogs.getBySessionId('{sess_id}')
        if (!logs.length) return null
        const items = await window.__repos.executionLogs.getItems(logs[0].id)
        return items.length ? {{reps: items[0].reps_done, weight: items[0].weight_used}} : null
    }}""")
    assert result is not None
    assert result["reps"] == 12
    assert result["weight"] == 55.0


def test_session_complete_after_last_step(page: Page, app_url: str, simple_session):
    """Completing all steps redirects back to the session detail view."""
    sess_id = simple_session["sess_id"]
    page.goto(f"{app_url}#/sessions/{sess_id}/execute")
    _wait_for_execute(page)

    page.click('[data-testid="execute-done-btn"]')
    page.wait_for_timeout(800)

    assert f"/sessions/{sess_id}" in page.url
    assert "/execute" not in page.url


def test_asymmetric_side_shown(page: Page, app_url: str, asymmetric_session):
    """Side toggle is visible for asymmetric exercises and hidden for non-asymmetric ones."""
    _wait_for_db(page, app_url)
    _clean_all(page)

    ex_sym = _create_exercise(page, "Squat Sym", default_reps=10)
    ex_asym = _create_exercise(page, "Lunge Asym", asymmetric=True, alternate=True, default_reps=10)
    sess_id = _create_session_with_exercises(page, "Side Test Session", [
        {"item_id": ex_sym, "item_type": "exercise", "repetitions": 10, "weight": 0},
        {"item_id": ex_asym, "item_type": "exercise", "repetitions": 10, "weight": 0},
    ])

    page.goto(f"{app_url}#/sessions/{sess_id}/execute")
    _wait_for_execute(page)

    # First step: non-asymmetric — no side toggle
    expect(page.locator('[data-testid="execute-side-toggle"]')).not_to_be_visible()

    page.click('[data-testid="execute-done-btn"]')
    page.wait_for_timeout(500)

    # Second step: asymmetric — side toggle visible
    side_toggle = page.locator('[data-testid="execute-side-toggle"]')
    expect(side_toggle).to_be_visible()
    assert "LEFT" in side_toggle.inner_text() or "RIGHT" in side_toggle.inner_text()

    # Toggle side
    initial = side_toggle.inner_text()
    side_toggle.click()
    page.wait_for_timeout(200)
    toggled = side_toggle.inner_text()
    assert initial != toggled


def test_asymmetric_side_logged(page: Page, app_url: str, asymmetric_session):
    """Side value is stored in execution_log_item.side for asymmetric exercises."""
    sess_id = asymmetric_session["sess_id"]
    page.goto(f"{app_url}#/sessions/{sess_id}/execute")
    _wait_for_execute(page)

    # Switch to RIGHT
    page.click('[data-testid="execute-side-toggle"]')
    page.wait_for_timeout(200)
    page.click('[data-testid="execute-done-btn"]')
    page.wait_for_timeout(800)

    result = page.evaluate(f"""async () => {{
        const logs = await window.__repos.executionLogs.getBySessionId('{sess_id}')
        if (!logs.length) return null
        const items = await window.__repos.executionLogs.getItems(logs[0].id)
        return items.length ? items[0].side : null
    }}""")
    assert result == "right"


def test_combo_rounds_auto_advance(page: Page, app_url: str, combo_session):
    """Between non-final rounds of a combo, no final-round screen appears."""
    sess_id = combo_session["sess_id"]
    page.goto(f"{app_url}#/sessions/{sess_id}/execute")
    _wait_for_execute(page)

    # Step 1: Squat Round 1
    page.click('[data-testid="execute-done-btn"]')
    page.wait_for_timeout(500)

    # Step 2: Jump Round 1 — after Done, should auto-advance to Round 2 (no pause screen)
    page.click('[data-testid="execute-done-btn"]')
    page.wait_for_timeout(500)

    # No final-round screen; we should be on Squat Round 2
    expect(page.locator('[data-testid="execute-final-round"]')).not_to_be_visible()
    expect(page.locator('[data-testid="execute-exercise-title"]')).to_have_text("Squat")
    combo_ctx = page.locator('[data-testid="execute-combo-context"]').inner_text()
    assert "2" in combo_ctx


def test_final_round_screen_shown(page: Page, app_url: str, combo_session):
    """Final-round screen appears after the last exercise of the last round."""
    sess_id = combo_session["sess_id"]
    page.goto(f"{app_url}#/sessions/{sess_id}/execute")
    _wait_for_execute(page)

    # Complete all 4 steps (2 exercises × 2 rounds)
    for _ in range(3):
        page.click('[data-testid="execute-done-btn"]')
        page.wait_for_timeout(500)

    # Step 4 — last exercise of last round
    page.click('[data-testid="execute-done-btn"]')
    page.wait_for_timeout(500)

    expect(page.locator('[data-testid="execute-final-round"]')).to_be_visible()
    expect(page.locator('[data-testid="execute-continue-btn"]')).to_be_visible()
    expect(page.locator('[data-testid="execute-one-more-btn"]')).to_be_visible()


def test_continue_advances_past_combo(page: Page, app_url: str, mixed_session):
    """Continue → after last combo round advances to the next session item."""
    sess_id = mixed_session["sess_id"]
    page.goto(f"{app_url}#/sessions/{sess_id}/execute")
    _wait_for_execute(page)

    # Solo Ex (step 1)
    page.click('[data-testid="execute-done-btn"]')
    page.wait_for_timeout(500)

    # Combo Ex1 (step 2, round 1 of 1)
    page.click('[data-testid="execute-done-btn"]')
    page.wait_for_timeout(500)

    # Combo Ex2 (step 3, last step of last round) → final-round screen
    page.click('[data-testid="execute-done-btn"]')
    page.wait_for_timeout(500)

    expect(page.locator('[data-testid="execute-final-round"]')).to_be_visible()

    # Click Continue → should finish session and redirect (no more items)
    page.click('[data-testid="execute-continue-btn"]')
    page.wait_for_timeout(800)

    assert f"/sessions/{sess_id}" in page.url
    assert "/execute" not in page.url


def test_one_more_round_appends_round(page: Page, app_url: str, combo_session):
    """One More Round inserts an extra round and updates the progress."""
    sess_id = combo_session["sess_id"]
    page.goto(f"{app_url}#/sessions/{sess_id}/execute")
    _wait_for_execute(page)

    # Complete all 4 steps to reach final-round screen
    for _ in range(4):
        page.click('[data-testid="execute-done-btn"]')
        page.wait_for_timeout(500)

    expect(page.locator('[data-testid="execute-final-round"]')).to_be_visible()

    # Click One More Round
    page.click('[data-testid="execute-one-more-btn"]')
    page.wait_for_timeout(500)

    # Should be on first exercise of new round
    progress = page.locator('[data-testid="execute-progress-label"]').inner_text()
    assert "/ 6" in progress  # 4 original + 2 new = 6 total

    combo_ctx = page.locator('[data-testid="execute-combo-context"]').inner_text()
    assert "3" in combo_ctx  # Round 3


def test_exit_mid_session_saves_partial_log(page: Page, app_url: str, two_step_session):
    """Exiting mid-session saves only the completed steps to the execution log."""
    sess_id = two_step_session["sess_id"]
    page.goto(f"{app_url}#/sessions/{sess_id}/execute")
    _wait_for_execute(page)

    # Complete step 1 only
    page.click('[data-testid="execute-done-btn"]')
    page.wait_for_timeout(500)

    # Exit mid-session
    page.click('[data-testid="execute-exit-btn"]')
    page.wait_for_timeout(800)

    assert f"/sessions/{sess_id}" in page.url
    assert "/execute" not in page.url

    result = page.evaluate(f"""async () => {{
        const logs = await window.__repos.executionLogs.getBySessionId('{sess_id}')
        if (!logs.length) return null
        const items = await window.__repos.executionLogs.getItems(logs[0].id)
        return {{items: items.length, finishedAt: logs[0].finished_at}}
    }}""")
    assert result is not None
    assert result["items"] == 1
    assert result["finishedAt"] is not None


def test_prefill_from_last_session_run(page: Page, app_url: str, simple_session):
    """Re-executing a session prefills reps/weight from the last run."""
    sess_id = simple_session["sess_id"]

    # First run with custom values
    page.goto(f"{app_url}#/sessions/{sess_id}/execute")
    _wait_for_execute(page)

    page.fill('[data-testid="execute-reps-input"]', "15")
    page.fill('[data-testid="execute-weight-input"]', "75")
    page.click('[data-testid="execute-done-btn"]')
    page.wait_for_timeout(800)

    # Second run — should prefill from last run
    page.goto(f"{app_url}#/sessions/{sess_id}/execute")
    _wait_for_execute(page)

    assert page.locator('[data-testid="execute-reps-input"]').input_value() == "15"
    assert page.locator('[data-testid="execute-weight-input"]').input_value() == "75"


def test_no_prior_log_uses_defaults(page: Page, app_url: str, simple_session):
    """With no prior log, session item defaults are used for prefill."""
    sess_id = simple_session["sess_id"]
    page.goto(f"{app_url}#/sessions/{sess_id}/execute")
    _wait_for_execute(page)

    # Default reps=8, weight=50 (from fixture)
    assert page.locator('[data-testid="execute-reps-input"]').input_value() == "8"
    assert page.locator('[data-testid="execute-weight-input"]').input_value() == "50"


def test_progress_bar_reflects_position(page: Page, app_url: str, two_step_session):
    """Progress label updates from 0/2 → 1/2 → redirects after 2/2."""
    sess_id = two_step_session["sess_id"]
    page.goto(f"{app_url}#/sessions/{sess_id}/execute")
    _wait_for_execute(page)

    expect(page.locator('[data-testid="execute-progress-label"]')).to_contain_text("0 / 2")

    page.click('[data-testid="execute-done-btn"]')
    page.wait_for_timeout(500)

    expect(page.locator('[data-testid="execute-progress-label"]')).to_contain_text("1 / 2")


def test_combo_context_shown(page: Page, app_url: str, combo_session):
    """Combo name and round number are shown in the step card for combo exercises."""
    sess_id = combo_session["sess_id"]
    page.goto(f"{app_url}#/sessions/{sess_id}/execute")
    _wait_for_execute(page)

    combo_ctx = page.locator('[data-testid="execute-combo-context"]')
    expect(combo_ctx).to_be_visible()
    ctx_text = combo_ctx.inner_text()
    assert "HIIT" in ctx_text
    assert "1" in ctx_text  # round 1
    assert "2" in ctx_text  # of 2
