#!/usr/bin/env python3
"""RC115 pass player scale real-DOM QA.

Uses actual preview.html + actual CSS/JS via page.set_content().
Checks the desktop media-card no longer stretches vertically to the info-card,
video remains a real 16:9 box, the obsolete mobile iOS/start overlay is hidden,
and custom video controls keep light text on dark backgrounds.
"""
from __future__ import annotations

import asyncio, base64, json, mimetypes, os, re, 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&enablejsapi=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 RC115 QA'));
  };
})();
</script>
"""
    html = html.replace("<head>", f"<head>{base}\n{qa_head}", 1)
    html = re.sub(r'<link\s+([^>]*rel=["\']stylesheet["\'][^>]*)>', lambda m: f"<style data-inline-src=\"{re.search(r'href=[\"\']([^\"\']+)[\"\']', m.group(1)).group(1)}\">\n{_read_asset(re.search(r'href=[\"\']([^\"\']+)[\"\']', m.group(1)).group(1))}\n</style>", html)
    html = re.sub(r'<script\s+([^>]*src=["\'][^"\']+["\'][^>]*)>\s*</script>', lambda m: f"<script data-inline-src=\"{re.search(r'src=[\"\']([^\"\']+)[\"\']', m.group(1)).group(1)}\">\n{_read_asset(re.search(r'src=[\"\']([^\"\']+)[\"\']', m.group(1)).group(1))}\n</script>", html)
    return html


def as_data_uri(src: str):
    if not src or src.startswith(("data:", "http://", "https://", "blob:")):
        return None
    clean = src.split("?", 1)[0].split("#", 1)[0]
    path = Path(clean[7:]) if clean.startswith("file://") else 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"
    return clean, f"data:{mime};base64,{base64.b64encode(path.read_bytes()).decode('ascii')}"


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})
    await page.wait_for_timeout(160)


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(12000)
    await page.set_content(html, wait_until="domcontentloaded")
    return ctx, page


async def render_video_state(page, *, playlist=False, current=4, force_overlay=False):
    await page.evaluate("""
      ({ playlist, current }) => {
        state.stage='Stage 2'; state.phase='Phase 1'; state.week='1'; state.day='Day 1';
        state.projectorNoYoutube=false; state.playlistVisible=!!playlist; state.tvMode=false; state.menuOpen=false;
        if(typeof startWorkout === 'function') startWorkout();
        workoutPrimed = true; state.current = current; state.inRest = false; state.restNextIndex = null;
        if(typeof renderStep === 'function') renderStep();
        if(typeof syncPassVideoControls === 'function') syncPassVideoControls();
      }
    """, {"playlist": playlist, "current": current})
    await page.wait_for_timeout(700)
    if force_overlay:
      await page.evaluate("() => { if(typeof showIOSPlayButton === 'function') showIOSPlayButton(); }")
      await page.wait_for_timeout(150)
    await inline_dom_images(page)


async def metrics(page):
    return await page.evaluate("""
      () => {
        const rect = el => el ? JSON.parse(JSON.stringify(el.getBoundingClientRect())) : null;
        const style = el => el ? getComputedStyle(el) : null;
        const media = document.querySelector('.next-pass-media-card');
        const wrap = document.getElementById('videoWrap');
        const controls = document.getElementById('passVideoControls');
        const info = document.querySelector('.next-pass-info-card');
        const overlay = document.getElementById('iosPlayBtn');
        const label = document.querySelector('#passVideoPlayBtn .pass-video-control-label');
        const icon = document.querySelector('#passVideoPlayBtn .pass-video-control-icon');
        const mr = rect(media), wr = rect(wrap), cr = rect(controls), ir = rect(info), or = rect(overlay);
        const visible = el => !!el && style(el).display !== 'none' && style(el).visibility !== 'hidden' && Number(style(el).opacity || '1') > 0.01;
        const ratio = wr ? wr.width / Math.max(1, wr.height) : 0;
        const mediaTail = (mr && cr) ? Math.max(0, mr.bottom - cr.bottom) : 0;
        const desktop = window.innerWidth >= 900;
        return {
          viewport: { width: window.innerWidth, height: window.innerHeight },
          title: document.getElementById('exerciseName')?.textContent?.trim() || '',
          videoSource: document.getElementById('exerciseScreen')?.dataset?.videoSource || '',
          mediaRect: mr,
          wrapRect: wr,
          controlsRect: cr,
          infoRect: ir,
          progressRect: rect(document.querySelector('.next-pass-progress-below')),
          playlistRect: rect(document.querySelector('.next-pass-playlist-card')),
          wrapAspect: ratio,
          wrapAspectOk: Math.abs(ratio - (16/9)) < 0.04,
          mediaTailAfterControls: mediaTail,
          mediaNotStretched: !desktop || mediaTail <= 8,
          infoCanBeTallerWithoutStretchingMedia: !desktop || !ir || !mr || ir.bottom >= mr.bottom - 2,
          overlayVisible: visible(overlay),
          overlayText: overlay?.textContent?.trim() || '',
          playLabelColor: style(label)?.color || '',
          playIconColor: style(icon)?.color || '',
          videoControlVisible: visible(controls),
          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 capture(page, name, w, h):
    out = OUT / f"rc115_pass_player_video_{name}_{w}x{h}.png"
    await page.screenshot(path=str(out), full_page=False)
    return f"qa_screenshots/{out.name}"


async def main():
    OUT.mkdir(exist_ok=True)
    html = build_inlined_preview()
    results = []
    from playwright.async_api import async_playwright
    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"])
        try:
            for name, w, h in VIEWPORTS:
                ctx, page = await setup_page(browser, html, w, h)
                try:
                    await render_video_state(page, playlist=(name == 'desktop'), current=4, force_overlay=(name == 'mobile'))
                    m = await metrics(page)
                    m["screenshot"] = await capture(page, name, w, h)
                    results.append(m)
                finally:
                    await ctx.close()
        finally:
            await browser.close()
    errors=[]
    for m in results:
        label=f"{m['viewport']['width']}x{m['viewport']['height']} {m.get('screenshot','')}"
        if m.get('brokenImages'):
            errors.append(label + f": broken images {m['brokenImages']}")
        if not m.get('videoControlVisible'):
            errors.append(label + ": video controls not visible")
        if not m.get('wrapAspectOk'):
            errors.append(label + f": video wrap aspect off {m.get('wrapAspect')}")
        if m['viewport']['width'] >= 900 and not m.get('mediaNotStretched'):
            errors.append(label + f": media card still stretched below controls by {m.get('mediaTailAfterControls')}px")
        if m['viewport']['width'] < 760 and m.get('overlayVisible'):
            errors.append(label + ": obsolete mobile tap overlay is visible")
        color = (m.get('playLabelColor') or '').lower()
        if m['viewport']['width'] < 760 and ('0, 0, 0' in color or '10, 15, 25' in color):
            errors.append(label + f": play label color still dark: {color}")
    payload={"ok": not errors, "method":"real preview DOM via page.set_content, actual CSS/JS, external network aborted, desktop with playlist visible", "results":results, "errors":errors}
    (OUT / 'rc115_pass_player_scale_dom_metrics.json').write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding='utf-8')
    if errors:
        raise SystemExit('\n'.join(errors))
    print(json.dumps({"ok": True, "screenshots": [m['screenshot'] for m in results]}, ensure_ascii=False))

if __name__ == '__main__':
    asyncio.run(main())
    os._exit(0)
