#!/usr/bin/env python3
"""RC114 pass media-card contract real-DOM QA.

Renders actual preview.html with inlined CSS/JS via Playwright page.set_content().
Verifies that the pass media-card is media-only for image/fallback states,
that premium images are contained inside the existing pass media card, and
that RC113 video controls still work for YouTube state.
"""
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&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 RC114 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})
    await page.wait_for_timeout(250)


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;
        state.playlistVisible=false;
        state.menuOpen=false;
      }
    """)


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)


async def render_image(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;
        state.projectorNoYoutube = true;
        state.current = 1; // Linear Pogos: OTA source + S2P1 premium direct-composite image.
        state.inRest = false;
        state.restNextIndex = null;
        if(typeof renderStep === 'function') renderStep();
      }
    """)
    await page.wait_for_timeout(850)
    await inline_dom_images(page)


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)


async def capture(page, stem: str, name: str, width: int, height: int):
    out = OUT / f"rc114_{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');
        const media = document.querySelector('.next-pass-media-card');
        const info = document.querySelector('.next-pass-info-card');
        const stage = document.querySelector('#videoWrap .next-pass-media-stage');
        const illustration = document.querySelector('#videoWrap .next-exercise-illustration');
        const img = document.querySelector('#videoWrap .next-exercise-visual-figure');
        const r = el => rect(el);
        const mediaRect = r(media);
        const imgRect = r(img);
        const infoRect = r(info);
        const visible = el => !!el && getComputedStyle(el).display !== 'none' && getComputedStyle(el).visibility !== 'hidden';
        const mediaText = (media?.innerText || '').replace(/\\s+/g, ' ').trim();
        const infoText = (info?.innerText || '').replace(/\\s+/g, ' ').trim();
        const eps = 1.5;
        return {
          screen: document.body.dataset.screen || '',
          bodyExerciseActive: document.body.classList.contains('exercise-active'),
          helperLoaded: !!window.NEXTRenderers?.passPlayer,
          passMode: screen?.dataset.passMode || '',
          videoSource: screen?.dataset.videoSource || '',
          wrapPassMode: wrap?.dataset.passMode || '',
          wrapVideoSource: wrap?.dataset.videoSource || '',
          exerciseName: document.getElementById('exerciseName')?.textContent?.trim() || '',
          nextText: document.getElementById('nextBtn')?.textContent?.trim() || '',
          mediaRect,
          infoRect,
          stageRect: r(stage),
          illustrationRect: r(illustration),
          imageRect: imgRect,
          videoControlsRect: r(document.getElementById('passVideoControls')),
          progressRect: r(document.querySelector('.next-pass-progress-below')),
          hasIframe: !!document.querySelector('#videoWrap iframe'),
          hasVideo: !!document.querySelector('#videoWrap video'),
          hasMediaOnly: !!document.querySelector('#videoWrap .next-pass-media-only'),
          hasMediaStage: !!stage,
          hasIllustration: !!illustration,
          hasPremiumImage: !!img,
          hasExerciseVisualCopy: !!document.querySelector('#videoWrap .exercise-visual-copy'),
          hasFallbackKicker: !!document.querySelector('#videoWrap .fallback-kicker'),
          videoControlsVisible: visible(document.getElementById('passVideoControls')),
          videoControlsAfterWrap: (() => { const controls=document.getElementById('passVideoControls'); return !!(wrap && controls && wrap.nextElementSibling === controls); })(),
          mediaText,
          infoText,
          mediaContainsExerciseNameText: !!(document.getElementById('exerciseName')?.textContent?.trim() && mediaText.includes(document.getElementById('exerciseName').textContent.trim())),
          infoContainsExerciseNameText: !!(document.getElementById('exerciseName')?.textContent?.trim() && infoText.includes(document.getElementById('exerciseName').textContent.trim())),
          mediaOverflowImage: !!(mediaRect && imgRect && (imgRect.left < mediaRect.left - eps || imgRect.right > mediaRect.right + eps || imgRect.top < mediaRect.top - eps || imgRect.bottom > mediaRect.bottom + eps)),
          imageOverlapsInfo: !!(imgRect && infoRect && !(imgRect.right <= infoRect.left + eps || imgRect.left >= infoRect.right - eps || imgRect.bottom <= infoRect.top + eps || imgRect.top >= infoRect.bottom - eps)),
          progressBelowLayout: (() => { const progress=document.querySelector('.next-pass-progress-below'); const layout=document.querySelector('.next-pass-layout'); if(!progress || !layout) return false; return progress.compareDocumentPosition(layout) & Node.DOCUMENT_POSITION_PRECEDING; })(),
          progressInsideTopbar: !!document.querySelector('.next-pass-topbar .next-pass-progress'),
          passVideoCommands: window.__passVideoCommands || [],
          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 = {"youtube": render_youtube, "image": render_image, "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"rc114_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)


def validate(results: dict):
    errors: list[str] = []
    for state_name in ("youtube", "image", "rest"):
        for m in results[state_name]:
            label = f"{state_name} {m.get('screenshot','')}"
            if not m.get("bodyExerciseActive"):
                errors.append(label + ": body is not exercise-active")
            if m.get("brokenImages"):
                errors.append(label + f": broken images {m['brokenImages']}")
            if m.get("progressInsideTopbar"):
                errors.append(label + ": progress moved back into topbar")
            if state_name == "youtube":
                if not m.get("hasIframe"):
                    errors.append(label + ": youtube iframe missing")
                if not m.get("videoControlsVisible"):
                    errors.append(label + ": video controls should be visible for youtube")
                funcs = [cmd.get("func") for cmd in m.get("passVideoCommands", [])]
                for fn in ("playVideo", "pauseVideo", "seekTo"):
                    if fn not in funcs:
                        errors.append(label + f": expected YouTube command {fn}")
            if state_name == "image":
                if m.get("videoSource") != "projector-blocked":
                    errors.append(label + f": image state should use projector-blocked fallback, got {m.get('videoSource')}")
                if not m.get("hasMediaOnly") or not m.get("hasMediaStage"):
                    errors.append(label + ": image state missing media-only/stage wrapper")
                if not m.get("hasIllustration") or not m.get("hasPremiumImage"):
                    errors.append(label + ": premium illustration not rendered")
                if m.get("hasExerciseVisualCopy") or m.get("hasFallbackKicker"):
                    errors.append(label + ": media card still contains duplicate copy/kicker")
                if m.get("mediaContainsExerciseNameText"):
                    errors.append(label + ": media card contains exercise name text")
                if not m.get("infoContainsExerciseNameText"):
                    errors.append(label + ": info card does not contain exercise name")
                if m.get("mediaOverflowImage"):
                    errors.append(label + ": premium image overflows media card")
                if m.get("imageOverlapsInfo"):
                    errors.append(label + ": premium image overlaps info card")
                if m.get("videoControlsVisible"):
                    errors.append(label + ": video controls visible for image-only state")
            if state_name == "rest":
                if m.get("videoSource") != "rest":
                    errors.append(label + f": rest source should be rest, got {m.get('videoSource')}")
                if m.get("videoControlsVisible"):
                    errors.append(label + ": video controls visible during rest")
    if errors:
        raise SystemExit("\n".join(errors))


async def main():
    OUT.mkdir(exist_ok=True)
    html = build_inlined_preview()
    results = {"method":"Chromium Playwright page.set_content with inlined preview.html/CSS/JS; single browser with isolated contexts per viewport/state; external iframes/network aborted except mocked local proxy fetches.", "youtube":[], "image":[], "rest":[]}
    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:
                for state_name, renderer in (("youtube", render_youtube), ("image", render_image), ("rest", render_rest)):
                    print(f"RC114 QA {state_name} {name} {w}x{h}", flush=True)
                    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)
                        results[state_name].append(m)
                    finally:
                        await ctx.close()
        finally:
            await browser.close()
    validate(results)
    (OUT / "rc114_pass_media_contract_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()
    else:
        asyncio.run(main())
        os._exit(0)
