#!/usr/bin/env python3
"""RC113 pass video controls real-DOM QA.

Renders actual preview.html with inlined CSS/JS via Playwright page.set_content().
Exercises the existing app.js player lifecycle while verifying RC113
pass video controls without changing video/timer lifecycle.
"""
from __future__ import annotations

import asyncio
import base64
import json
import mimetypes
import os
import re
import subprocess
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "qa_screenshots"
VIEWPORTS = [
    ("mobile", 390, 844),
    ("ipad", 820, 1180),
    ("desktop", 1440, 1024),
]


def _read_asset(src: str) -> str:
    clean = src.split("?", 1)[0].split("#", 1)[0]
    return (ROOT / clean).read_text(encoding="utf-8")


def build_inlined_preview() -> str:
    html = (ROOT / "preview.html").read_text(encoding="utf-8")
    base = f'<base href="file://{ROOT.as_posix()}/">'
    qa_head = """
<script>
(function(){
  try{ history.replaceState = function(){}; history.pushState = function(){}; }catch(e){}
  const nativeFetch = window.fetch ? window.fetch.bind(window) : null;
  window.fetch = function(url, options){
    const s = String(url || '');
    if(s.includes('ota_youtube_proxy.php')){
      return Promise.resolve({ ok:true, json:function(){ return Promise.resolve({ embedUrl:'https://www.youtube.com/embed/TESTID12345?rel=0&modestbranding=1', watchUrl:'https://www.youtube.com/watch?v=TESTID12345', youtubeId:'TESTID12345' }); } });
    }
    if(s.includes('video_backup_api.php')){
      return Promise.resolve({ ok:true, json:function(){ return Promise.resolve({ status:'missing', exists:false }); } });
    }
    return nativeFetch ? nativeFetch(url, options) : Promise.reject(new Error('fetch disabled in RC113 QA'));
  };
})();
</script>
"""
    html = html.replace("<head>", f"<head>{base}\n{qa_head}", 1)

    def repl_link(match: re.Match[str]) -> str:
        attrs = match.group(1)
        href_match = re.search(r'href=["\']([^"\']+)["\']', attrs)
        if not href_match:
            return match.group(0)
        href = href_match.group(1)
        css = _read_asset(href)
        return f"<style data-inline-src=\"{href}\">\n{css}\n</style>"

    html = re.sub(r'<link\s+([^>]*rel=["\']stylesheet["\'][^>]*)>', repl_link, html)

    def repl_script(match: re.Match[str]) -> str:
        attrs = match.group(1)
        src_match = re.search(r'src=["\']([^"\']+)["\']', attrs)
        if not src_match:
            return match.group(0)
        src = src_match.group(1)
        js = _read_asset(src)
        return f"<script data-inline-src=\"{src}\">\n{js}\n</script>"

    html = re.sub(r'<script\s+([^>]*src=["\'][^"\']+["\'][^>]*)>\s*</script>', repl_script, html)
    return html


def as_data_uri(src: str) -> tuple[str, str] | None:
    if not src or src.startswith(("data:", "http://", "https://", "blob:")):
        return None
    clean = src.split("?", 1)[0].split("#", 1)[0]
    if clean.startswith("file://"):
        path = Path(clean[7:])
    else:
        path = ROOT / clean.lstrip("/")
    if not path.exists() or not path.is_file():
        return None
    mime = mimetypes.guess_type(path.name)[0] or "application/octet-stream"
    data = base64.b64encode(path.read_bytes()).decode("ascii")
    return clean, f"data:{mime};base64,{data}"


async def inline_dom_images(page):
    imgs = await page.evaluate("""
      () => Array.from(document.images).map((img, i) => ({ i, src: img.getAttribute('src') || '' }))
    """)
    for img in imgs:
        converted = as_data_uri(img["src"])
        if not converted:
            continue
        original, data_uri = converted
        await page.evaluate("""
          ({ index, original, dataUri }) => {
            const img = document.images[index];
            if(!img) return;
            img.setAttribute('data-qa-original-src', original);
            img.src = dataUri;
          }
        """, {"index": img["i"], "original": original, "dataUri": data_uri})


async def setup_page(browser, html: str, width: int, height: int):
    ctx = await browser.new_context(viewport={"width": width, "height": height}, device_scale_factor=1)

    async def _route(route):
        url = route.request.url
        if url.startswith(("http://", "https://")):
            await route.abort()
        else:
            await route.continue_()

    await ctx.route("**/*", _route)
    page = await ctx.new_page()
    page.set_default_timeout(15000)
    page.set_default_navigation_timeout(15000)
    await page.set_content(html, wait_until="domcontentloaded")
    await page.evaluate("""
      () => {
        window.__passVideoCommands = [];
        if(typeof ytCommandTo === 'function'){
          const original = ytCommandTo;
          window.__originalYtCommandTo = original;
          ytCommandTo = function(frameId, func, args){
            window.__passVideoCommands.push({ frameId, func, args: Array.isArray(args) ? args : [] });
            return original(frameId, func, args);
          };
        }
      }
    """)
    return ctx, page


async def set_stage2_day1(page):
    await page.evaluate("""
      () => {
        state.stage='Stage 2';
        state.phase='Phase 1';
        state.week='1';
        state.day='Day 1';
        state.current=0;
        state.inRest=false;
        state.restNextIndex=null;
        state.projectorNoYoutube=false;
      }
    """)


async def render_start(page):
    await set_stage2_day1(page)
    await page.evaluate("""
      () => { if(typeof startWorkout === 'function') startWorkout(); }
    """)
    await page.wait_for_timeout(850)
    await inline_dom_images(page)
    await page.wait_for_timeout(150)


async def render_youtube(page):
    await set_stage2_day1(page)
    await page.evaluate("""
      () => { if(typeof startWorkout === 'function') startWorkout(); }
    """)
    await page.wait_for_timeout(450)
    await page.evaluate("""
      () => { if(typeof primeWorkoutStart === 'function') primeWorkoutStart(); }
    """)
    await page.wait_for_timeout(850)
    await page.evaluate("""
      () => {
        if(typeof syncPassVideoControls === 'function') syncPassVideoControls();
        if(typeof passVideoPlay === 'function') passVideoPlay();
        if(typeof passVideoPause === 'function') passVideoPause();
        window.postMessage(JSON.stringify({event:'infoDelivery', info:{currentTime:42, playerState:2}}), '*');
        if(typeof passVideoRewind === 'function') passVideoRewind(10);
      }
    """)
    await page.wait_for_timeout(300)
    await inline_dom_images(page)
    await page.wait_for_timeout(150)


async def render_rest(page):
    await set_stage2_day1(page)
    await page.evaluate("""
      () => { if(typeof startWorkout === 'function') startWorkout(); }
    """)
    await page.wait_for_timeout(450)
    await page.evaluate("""
      () => {
        workoutPrimed = true;
        if(typeof renderRest === 'function') renderRest(30, 1);
      }
    """)
    await page.wait_for_timeout(850)
    await inline_dom_images(page)
    await page.wait_for_timeout(150)


async def capture(page, stem: str, name: str, width: int, height: int):
    out = OUT / f"rc113_{stem}_{name}_{width}x{height}.png"
    await page.screenshot(path=str(out), full_page=False)
    return f"qa_screenshots/{out.name}"


async def metrics(page):
    return await page.evaluate("""
      () => {
        const rect = el => el ? JSON.parse(JSON.stringify(el.getBoundingClientRect())) : null;
        const screen = document.getElementById('exerciseScreen');
        const wrap = document.getElementById('videoWrap');
        return {
          screen: document.body.dataset.screen || '',
          bodyExerciseActive: document.body.classList.contains('exercise-active'),
          helperLoaded: !!window.NEXTRenderers?.passPlayer,
          passScreenClass: screen?.className || '',
          passMode: screen?.dataset.passMode || '',
          videoSource: screen?.dataset.videoSource || '',
          wrapClass: wrap?.className || '',
          wrapPassMode: wrap?.dataset.passMode || '',
          wrapVideoSource: wrap?.dataset.videoSource || '',
          hasStartStage: !!document.querySelector('.next-pass-start-stage.start-stage'),
          hasRestStage: !!document.querySelector('.next-pass-rest-stage.rest-stage'),
          hasFallback: !!document.querySelector('.next-pass-fallback'),
          hasIframe: !!document.querySelector('#videoWrap iframe'),
          hasVideo: !!document.querySelector('#videoWrap video'),
          mediaRect: rect(document.querySelector('.next-pass-media-card')),
          infoRect: rect(document.querySelector('.next-pass-info-card')),
          controlsRect: rect(document.querySelector('.next-pass-controls')),
          topbarRect: rect(document.querySelector('.next-pass-topbar')),
          progressRect: rect(document.querySelector('.next-pass-progress-below')),
          prevRect: rect(document.getElementById('prevBtn')),
          nextRect: rect(document.getElementById('nextBtn')),
          menuRect: rect(document.getElementById('menuToggleBtn')),
          videoControlsRect: rect(document.getElementById('passVideoControls')),
          rewindRect: rect(document.getElementById('passVideoRewindBtn')),
          playRect: rect(document.getElementById('passVideoPlayBtn')),
          pauseRect: rect(document.getElementById('passVideoPauseBtn')),
          videoControlsVisible: !!document.getElementById('passVideoControls') && getComputedStyle(document.getElementById('passVideoControls')).display !== 'none',
          videoControlsAriaHidden: document.getElementById('passVideoControls')?.getAttribute('aria-hidden') || '',
          videoControlsAfterWrap: (() => { const wrap=document.getElementById('videoWrap'); const controls=document.getElementById('passVideoControls'); return !!(wrap && controls && wrap.nextElementSibling === controls); })(),
          rewindDisabled: !!document.getElementById('passVideoRewindBtn')?.disabled,
          playDisabled: !!document.getElementById('passVideoPlayBtn')?.disabled,
          pauseDisabled: !!document.getElementById('passVideoPauseBtn')?.disabled,
          passVideoCommands: window.__passVideoCommands || [],
          progressInsideTopbar: !!document.querySelector('.next-pass-topbar .next-pass-progress'),
          controlsInsideTopbar: !!document.querySelector('.next-pass-topbar .next-pass-controls'),
          topControlOrder: Array.from(document.querySelectorAll('.next-pass-topbar .next-pass-controls > *')).map(el => el.id || el.className),
          exerciseName: document.getElementById('exerciseName')?.textContent?.trim() || '',
          nextText: document.getElementById('nextBtn')?.textContent?.trim() || '',
          brokenImages: Array.from(document.images).filter(img => img.naturalWidth === 0 && !String(img.src || '').startsWith('http')).map(img => img.getAttribute('data-qa-original-src') || img.getAttribute('src'))
        };
      }
    """)


async def run_single_state(state_name: str, name: str, w: int, h: int):
    from playwright.async_api import async_playwright
    html = build_inlined_preview()
    renderer = {"start": render_start, "youtube": render_youtube, "rest": render_rest}[state_name]
    async with async_playwright() as p:
        browser = await p.chromium.launch(
            executable_path="/usr/bin/chromium",
            headless=True,
            args=["--no-sandbox", "--disable-dev-shm-usage", "--disable-gpu", "--disable-gpu-sandbox", "--use-gl=swiftshader", "--enable-unsafe-swiftshader"],
        )
        ctx, page = await setup_page(browser, html, w, h)
        try:
            await renderer(page)
            m = await metrics(page)
            m["screenshot"] = await capture(page, f"pass_player_{state_name}", name, w, h)
            metric_file = OUT / f"rc113_metric_{state_name}_{name}_{w}x{h}.json"
            metric_file.write_text(json.dumps(m, ensure_ascii=False, indent=2), encoding="utf-8")
        finally:
            try:
                await ctx.close()
                await browser.close()
            except Exception:
                pass


def child_main():
    OUT.mkdir(exist_ok=True)
    _, _, state_name, name, w, h = sys.argv
    asyncio.run(run_single_state(state_name, name, int(w), int(h)))
    os._exit(0)


async def main():
    OUT.mkdir(exist_ok=True)
    script = Path(__file__).resolve()
    results = {"method":"Chromium Playwright page.set_content with inlined preview.html/CSS/JS; each viewport/state isolated in a child process to avoid iframe/timer bleed; external iframes/network aborted except mocked local proxy fetches.", "start":[], "youtube":[], "rest":[]}
    for name, w, h in VIEWPORTS:
        for state_name in ("start", "youtube", "rest"):
            print(f"RC113 QA {state_name} {name} {w}x{h}", flush=True)
            metric_file = OUT / f"rc113_metric_{state_name}_{name}_{w}x{h}.json"
            if metric_file.exists():
                metric_file.unlink()
            subprocess.run([sys.executable, str(script), "--child", state_name, name, str(w), str(h)], cwd=str(ROOT), check=True, timeout=90)
            results[state_name].append(json.loads(metric_file.read_text(encoding="utf-8")))
    (OUT / "rc113_pass_player_dom_metrics.json").write_text(json.dumps(results, ensure_ascii=False, indent=2), encoding="utf-8")
    print(json.dumps({"ok": True, "counts": {k: len(v) for k, v in results.items() if isinstance(v, list)}}, ensure_ascii=False), flush=True)


if __name__ == "__main__":
    if len(sys.argv) > 1 and sys.argv[1] == "--child":
        child_main()
    print("Usage: python3 tests/rc113_pass_video_controls_real_dom_qa.py --child <start|youtube|rest> <mobile|ipad|desktop> <width> <height>", flush=True)
    os._exit(0)
