"""
Step 15 — Audio Cues Tests

The WebAudio API is replaced by a lightweight mock on every page load so that
each beep is recorded in window._audioLog as {freq: number}:
  - 440 Hz  →  soft bip
  - 880 Hz  →  loud BIP

page.clock.install() controls fake time so setInterval ticks are driven
explicitly with page.clock.run_for(ms), making timer-dependent assertions
instant and deterministic.

TABATA audio cues
- test_tabata_bip_at_half_work_interval:      440 Hz bip fires at half of workTime
- test_tabata_countdown_end_of_work_phase:    bip,bip,BIP at the end of the work phase
- test_tabata_bip_at_half_rest_interval:      440 Hz bip fires at half of restTime
- test_tabata_countdown_end_of_rest_phase:    bip,bip,BIP at the end of the rest phase

AMRAP audio cues
- test_amrap_minute_mark_plays_bip:           soft bip (not BIP) fires at each elapsed minute
- test_amrap_half_time_plays_BIP:             loud BIP fires at exactly the halfway point
- test_amrap_last_minute_countdown:           bip at r=2, bip at r=1, BIP at expiry
"""

import json
import pytest
from playwright.sync_api import Page, expect

# ── Audio mock injected before every page load ────────────────────────────────

_AUDIO_MOCK = """
window._audioLog = [];
window.AudioContext = function () {
    return {
        state: 'running',
        currentTime: 0,
        createOscillator: function () {
            let _freq = 0;
            return {
                connect: function () {},
                frequency: {
                    get value() { return _freq; },
                    set value(v) { _freq = v; }
                },
                start: function () {},
                stop: function () { window._audioLog.push({ freq: _freq }); }
            };
        },
        createGain: function () {
            return { connect: function () {}, gain: { value: 0 } };
        },
        resume: function () { return Promise.resolve(); },
        destination: {}
    };
};
"""


# ── Shared helpers ────────────────────────────────────────────────────────────


def _setup(page: Page, app_url: str):
    """Install fake clock and audio mock, then load the app."""
    page.clock.install()
    page.add_init_script(_AUDIO_MOCK)
    page.goto(app_url)
    page.wait_for_function(
        "document.documentElement.getAttribute('data-db-ready') === 'true'",
        timeout=15000,
    )


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


def _create_timed_combo(page: Page, title: str, combo_type: str,
                         exercise_ids: list, timer_config: dict = None) -> str:
    tc = json.dumps(timer_config or {})
    exs = json.dumps([{"exerciseId": eid, "defaultReps": 10, "defaultWeight": 0}
                      for eid in exercise_ids])
    return page.evaluate(f"""async () => {{
        const uid = await window.__repos.settings.get('user_uuid')
        const cid = await window.__repos.combos.create({{
            title: {repr(title)}, description: '', type: {repr(combo_type)},
            timer_config: {tc}, created_by: uid
        }})
        await window.__repos.combos.setExercises(cid, {exs})
        return cid
    }}""")


def _create_session(page: Page, title: str, items: list) -> str:
    items_json = json.dumps(items)
    return page.evaluate(f"""async () => {{
        const uid = await window.__repos.settings.get('user_uuid')
        const sid = await window.__repos.sessions.create({{
            title: {repr(title)}, description: '', created_by: uid
        }})
        const items = {items_json}
        await window.__repos.sessions.setItems(sid, items.map((it, i) => ({{
            item_id: it.item_id, item_type: it.item_type,
            position: i, repetitions: it.repetitions, weight: 0
        }})))
        return sid
    }}""")


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


def _navigate_to_execute(page: Page, app_url: str, sess_id: str):
    page.goto(f"{app_url}#/sessions/{sess_id}/execute")
    page.wait_for_selector('[data-testid="execute-session-title"]', timeout=10000)
    page.wait_for_timeout(400)


def _freqs(page: Page) -> list[int]:
    """Return the list of recorded frequencies from the audio mock."""
    return [e["freq"] for e in page.evaluate("window._audioLog")]


# ── TABATA audio cue tests ────────────────────────────────────────────────────


def test_tabata_bip_at_half_work_interval(page: Page, app_url: str, browser_name: str):
    """A soft bip (440 Hz) fires at exactly the half-interval point of the work phase."""
    _setup(page, app_url)
    _clean_all(page)

    ex_id = _create_exercise(page, "Tab HalfWork Ex")
    # workTime=20 → half=10; restTime=60 so we stay in work phase during the test
    combo_id = _create_timed_combo(page, "Tab HalfWork", "TABATA", [ex_id],
                                    timer_config={"workTime": 20, "restTime": 60})
    sess_id = _create_session(page, "Tab HalfWork Session",
                               [{"item_id": combo_id, "item_type": "combo", "repetitions": 1}])

    _navigate_to_execute(page, app_url, sess_id)
    # Immediately after load: BIP(880) fires for work phase start

    page.clock.run_for(10_000)   # advance to r=10 — exactly the half-interval
    page.wait_for_timeout(300)

    f = _freqs(page)
    # Expected so far: BIP at t=0, bip at t=10 (half)
    assert f.count(880) == 1, f"Expected 1 BIP (work start) so far: {f}"
    assert f.count(440) == 1, f"Expected exactly 1 bip at half-interval: {f}"
    assert f == [880, 440], f"Unexpected audio sequence: {f}"


def test_tabata_countdown_end_of_work_phase(page: Page, app_url: str, browser_name: str):
    """bip, bip, BIP fires at the last 2 seconds of the work phase (1-second spacing)."""
    _setup(page, app_url)
    _clean_all(page)

    ex_id = _create_exercise(page, "Tab CntdwnWork Ex")
    # workTime=10 → half=5, countdown at r=2 and r=1, BIP at rest start
    combo_id = _create_timed_combo(page, "Tab CntdwnWork", "TABATA", [ex_id],
                                    timer_config={"workTime": 10, "restTime": 60})
    sess_id = _create_session(page, "Tab CntdwnWork Session",
                               [{"item_id": combo_id, "item_type": "combo", "repetitions": 1}])

    _navigate_to_execute(page, app_url, sess_id)

    page.clock.run_for(10_000)   # full work phase
    page.wait_for_timeout(300)

    f = _freqs(page)
    # BIP(work start), bip(r=5 half), bip(r=2), bip(r=1), BIP(rest start)
    assert f == [880, 440, 440, 440, 880], f"Unexpected audio sequence: {f}"


def test_tabata_bip_at_half_rest_interval(page: Page, app_url: str, browser_name: str):
    """A soft bip (440 Hz) fires at the half-interval point of the rest phase."""
    _setup(page, app_url)
    _clean_all(page)

    ex_id = _create_exercise(page, "Tab HalfRest Ex")
    # workTime=10 (half=5), restTime=20 (half=10)
    combo_id = _create_timed_combo(page, "Tab HalfRest", "TABATA", [ex_id],
                                    timer_config={"workTime": 10, "restTime": 20})
    sess_id = _create_session(page, "Tab HalfRest Session",
                               [{"item_id": combo_id, "item_type": "combo", "repetitions": 1}])

    _navigate_to_execute(page, app_url, sess_id)

    # 10s work + 10s into rest (= rest half-interval)
    page.clock.run_for(20_000)
    page.wait_for_timeout(300)

    f = _freqs(page)
    # Work:  BIP(start) bip(r=5) bip(r=2) bip(r=1) BIP(rest start)
    # Rest (first 10 s): bip(r=10 half)
    assert f == [880, 440, 440, 440, 880, 440], f"Unexpected audio sequence: {f}"


def test_tabata_countdown_end_of_rest_phase(page: Page, app_url: str, browser_name: str):
    """bip, bip, BIP fires at the last 2 seconds of the rest phase."""
    _setup(page, app_url)
    _clean_all(page)

    ex1 = _create_exercise(page, "Tab CntdwnRest Ex1")
    ex2 = _create_exercise(page, "Tab CntdwnRest Ex2")
    # workTime=10 (half=5), restTime=10 (half=5); two exercises to verify rest→work BIP
    combo_id = _create_timed_combo(page, "Tab CntdwnRest", "TABATA", [ex1, ex2],
                                    timer_config={"workTime": 10, "restTime": 10})
    sess_id = _create_session(page, "Tab CntdwnRest Session",
                               [{"item_id": combo_id, "item_type": "combo", "repetitions": 1}])

    _navigate_to_execute(page, app_url, sess_id)

    # 10s work + 10s rest
    page.clock.run_for(20_000)
    page.wait_for_timeout(300)

    f = _freqs(page)
    # Work:  BIP bip(r=5) bip(r=2) bip(r=1) BIP(rest start)
    # Rest:  bip(r=5) bip(r=2) bip(r=1) BIP(work2 start)
    assert f == [880, 440, 440, 440, 880, 440, 440, 440, 880], \
        f"Unexpected audio sequence: {f}"


# ── AMRAP audio cue tests ─────────────────────────────────────────────────────


def test_amrap_minute_mark_plays_bip(page: Page, app_url: str, browser_name: str):
    """Soft bip (440 Hz, not BIP) fires at each elapsed minute outside the last minute."""
    _setup(page, app_url)
    _clean_all(page)

    ex_id = _create_exercise(page, "AMRAP Min Bip Ex")
    # 4-min AMRAP (240 s): halfDuration=120; first minute fires at r=180 (t=60)
    combo_id = _create_timed_combo(page, "AMRAP Min Bip", "AMRAP", [ex_id])
    sess_id = _create_session(page, "AMRAP Min Bip Session",
                               [{"item_id": combo_id, "item_type": "combo", "repetitions": 4}])

    _navigate_to_execute(page, app_url, sess_id)
    # AMRAP plays no BIP at start

    page.clock.run_for(61_000)   # past the 1-minute mark, before half-time (120 s)
    page.wait_for_timeout(300)

    f = _freqs(page)
    assert 880 not in f, f"BIP should not fire before half-time: {f}"
    assert f.count(440) == 1, f"Expected exactly 1 bip at the 1-minute mark: {f}"


def test_amrap_half_time_plays_BIP(page: Page, app_url: str, browser_name: str):
    """Loud BIP (880 Hz) fires at the halfway point of the total AMRAP duration."""
    _setup(page, app_url)
    _clean_all(page)

    ex_id = _create_exercise(page, "AMRAP Half BIP Ex")
    # 4-min AMRAP (240 s): halfDuration = round(240/2) = 120 s
    # At t=60 (r=180): bip for 1-min mark
    # At t=120 (r=120): BIP for half-time (also coincides with 2-min mark, BIP takes priority)
    combo_id = _create_timed_combo(page, "AMRAP Half BIP", "AMRAP", [ex_id])
    sess_id = _create_session(page, "AMRAP Half BIP Session",
                               [{"item_id": combo_id, "item_type": "combo", "repetitions": 4}])

    _navigate_to_execute(page, app_url, sess_id)

    page.clock.run_for(121_000)  # past both 1-min bip (t=60) and half-time BIP (t=120)
    page.wait_for_timeout(300)

    f = _freqs(page)
    assert 880 in f, f"Half-time BIP should have fired: {f}"
    assert f.count(880) == 1, f"Expected exactly 1 BIP (half-time), got {f.count(880)}: {f}"
    # The minute bip at t=60 must precede the half-time BIP at t=120
    assert f.index(440) < f.index(880), f"Minute bip should precede half-time BIP: {f}"


def test_amrap_last_minute_countdown(page: Page, app_url: str, browser_name: str):
    """bip at r=2 and r=1 lead into the final BIP when AMRAP time expires (1-second spacing)."""
    _setup(page, app_url)
    _clean_all(page)

    ex_id = _create_exercise(page, "AMRAP LastMin Ex")
    # 3-min AMRAP (180 s): halfDuration=90
    # Full expected sequence:
    #   bip at t=60  (r=120, 1-min mark)
    #   BIP at t=90  (r=90,  half-time)
    #   bip at t=150 (r=30,  midpoint of last minute)
    #   bip at t=178 (r=2,   countdown)
    #   bip at t=179 (r=1,   countdown)
    #   BIP at t=180 (r=0,   expiry)
    combo_id = _create_timed_combo(page, "AMRAP LastMin", "AMRAP", [ex_id])
    sess_id = _create_session(page, "AMRAP LastMin Session",
                               [{"item_id": combo_id, "item_type": "combo", "repetitions": 3}])

    _navigate_to_execute(page, app_url, sess_id)

    # Advance the full AMRAP duration in one step for deterministic results
    page.clock.run_for(180_000)
    page.wait_for_timeout(300)

    f = _freqs(page)
    assert f == [440, 880, 440, 440, 440, 880], f"Unexpected full AMRAP audio sequence: {f}"
    # Verify the bip,bip,BIP countdown is the final 3 sounds
    assert f[-3:] == [440, 440, 880], f"Last 3 sounds should be bip,bip,BIP: {f}"
