"""Playwright E2E for the audio dial + Video toggle (audio spec §9). Skips
cleanly when Playwright or its browser binary is absent.

NOTE: headless engines relax BOTH autoplay and GPU compositing, so these assert
the WIRING (boot-silent, video-on couples audio, dial→play/volume, video-off
blanking) — real Safari/iOS autoplay and real-GPU compositing still need a device
by-ear/eye check.

The experience BOOTS SILENT — video off, audio dial at 0 — so nothing plays until a
real user gesture. Turning Video ON (a click) couples the audio dial up to 3/10 and
starts the soundtrack IN that gesture, sidestepping the browser autoplay block that
swallowed an un-gestured auto-start. Audio is a 0–10 range dial (not a checkbox);
set it by writing `value` + firing an `input` event.

Install:  pip install -e '.[e2e]' && python -m playwright install chromium
"""
import socket
import threading
import time
from contextlib import closing

import pytest

pytest.importorskip("playwright.sync_api")


def _free_port() -> int:
    with closing(socket.socket()) as s:
        s.bind(("127.0.0.1", 0))
        return s.getsockname()[1]


@pytest.fixture(scope="module")
def app_url():
    import uvicorn

    from simulator.app import app

    port = _free_port()
    config = uvicorn.Config(app, host="127.0.0.1", port=port, log_level="error")
    server = uvicorn.Server(config)
    thread = threading.Thread(target=server.run, daemon=True)
    thread.start()
    for _ in range(50):
        if server.started:
            break
        time.sleep(0.1)
    if not server.started:
        pytest.skip("uvicorn did not start")
    yield f"http://127.0.0.1:{port}"
    server.should_exit = True
    thread.join(timeout=5)


@pytest.fixture
def page(app_url):
    from playwright.sync_api import sync_playwright

    with sync_playwright() as p:
        try:
            # headless blocks media playback without a device; this flag lets play()
            # actually start so `paused` is meaningful (spec §9 asserts src/paused)
            browser = p.chromium.launch(args=["--autoplay-policy=no-user-gesture-required"])
        except Exception as exc:                       # no browser binary installed
            pytest.skip(f"chromium not available: {exc}")
        pg = browser.new_page()
        pg.goto(app_url)
        # wait out the "Loading Universe…" splash (it overlays + blocks clicks); the
        # experience boots silent (video off, audio 0) as it dismisses.
        pg.wait_for_selector("#loading", state="detached", timeout=60000)
        yield pg
        browser.close()


def _toggle_visual(page):
    """Click the visible Video toggle track (the checkbox input is hidden)."""
    page.click("label[for='visual'] .dev-switch-track")


def _set_audio(page, level):
    """Drive the 0–10 audio dial: set value + fire `input` (the engine reconciles
    playback synchronously in that gesture)."""
    page.evaluate(
        "(lvl) => { const a = document.getElementById('audio');"
        " a.value = String(lvl); a.dispatchEvent(new Event('input', {bubbles:true})); }",
        level,
    )


def test_app_boots_silent_video_off(page):
    # The experience boots SILENT: Video off, audio dial at 0, screen black, both
    # crossfade elements paused — nothing plays until a real user gesture.
    assert page.is_checked("#visual") is False
    assert page.evaluate("document.getElementById('audio').value") == "0"
    assert page.evaluate("document.getElementById('audio-level-val').textContent") == "0"
    page.wait_for_selector("#black:not(.hidden)")                      # screen blanked
    assert page.evaluate(
        "(() => { const a = document.getElementById('aud'), b = document.getElementById('aud-b');"
        " return a.paused && b.paused; })()"
    ) is True


def test_video_on_couples_audio_to_three_and_plays(page):
    # Turning Video on (a click gesture) lifts the audio dial to 3/10 and starts the
    # soundtrack IN that gesture: video shows (black hidden), a scale soundtrack plays.
    _toggle_visual(page)                                                # off -> on
    page.wait_for_function("document.getElementById('audio').value === '3'")
    page.wait_for_function("document.getElementById('audio-level-val').textContent === '3'")
    page.wait_for_selector("#black.hidden", state="attached")          # video showing
    page.wait_for_function(
        "(() => { const a = document.getElementById('aud');"
        " return /\\/media\\/audio\\/.+\\.mp3/.test(a.src) && !a.paused; })()"
    )


def test_audio_dial_sets_level_and_label(page):
    _set_audio(page, 7)
    page.wait_for_function("document.getElementById('audio').value === '7'")
    page.wait_for_function("document.getElementById('audio-level-val').textContent === '7'")


def test_audio_dial_zero_silences(page):
    # Level 0 fades both crossfade elements to silence, then pauses them.
    _set_audio(page, 0)
    page.wait_for_function(
        "(() => { const a = document.getElementById('aud'), b = document.getElementById('aud-b');"
        " return a.paused && b.paused; })()"
    )
    assert page.evaluate("document.getElementById('audio').value") == "0"


def test_video_off_blanks(page):
    # Turn Video on (boot is off), then off again: that blanks the screen and hides
    # the GPU layers.
    _toggle_visual(page)                                                # off -> on
    page.wait_for_selector("#black.hidden", state="attached")
    _toggle_visual(page)                                                # on -> off
    page.wait_for_selector("#black:not(.hidden)")
    page.wait_for_function(
        "['vid','paint'].every(id => getComputedStyle(document.getElementById(id)).opacity === '0')"
    )


def test_soundtrack_fallback_when_ring_lacks_audio(page):
    # Client resilience: even if /api/ring omits the per-scale `audio` field, the
    # scale-id fallback map keeps a soundtrack playing. Drop the field, re-apply the
    # dial, and confirm the cosmos soundtrack still resolves + plays.
    page.evaluate("ring.scales.forEach(s => { delete s.audio; })")
    _set_audio(page, 5)
    page.wait_for_function(
        "(() => { const a = document.getElementById('aud');"
        " return a.src.includes('/media/audio/cosmos/') && !a.paused; })()"
    )
