#!/usr/bin/env python3
"""RC119 legacy cleanup / v97 handoff real-DOM QA."""
from __future__ import annotations
import asyncio, base64, json, mimetypes, os, re
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "qa_screenshots"


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 RC119 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(100)


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_active_pass(page, *, current=4):
    await page.evaluate("""
      ({ current }) => {
        state.stage='Stage 2'; state.phase='Phase 1'; state.week='1'; state.day='Day 1';
        state.projectorNoYoutube=false; state.playlistVisible=true; state.tvMode=false; state.liveMode=false; state.menuOpen=false;
        if(typeof startWorkout === 'function') startWorkout();
        workoutPrimed = true; state.current = current; state.inRest = false; state.restNextIndex = null; state.playlistVisible=true;
        if(typeof renderStep === 'function') renderStep();
        if(typeof renderControls === 'function') renderControls();
      }
    """, {"current": current})
    await page.wait_for_timeout(400)
    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 menu = document.getElementById('settingsMenu');
        const topbar = document.querySelector('.next-pass-topbar');
        const layout = document.querySelector('.next-pass-layout');
        const media = document.querySelector('.next-pass-media-card');
        const info = document.querySelector('.next-pass-info-card');
        const playlist = document.querySelector('.next-pass-playlist-card');
        const toggles = Array.from(document.querySelectorAll('.next-settings-toggle'));
        const groups = Array.from(document.querySelectorAll('.next-settings-group')).map(el => ({ key:el.dataset.settingsGroup || '', title:el.querySelector('.next-settings-group-title')?.textContent.trim() || '' }));
        return {
          body:{tv:document.body.classList.contains('tv-mode'), live:document.body.classList.contains('live-mode'), playlistVisible:document.body.classList.contains('playlist-visible'), playlistHidden:document.body.classList.contains('playlist-hidden')},
          state:{tv:!!state.tvMode, live:!!state.liveMode, playlistVisible:!!state.playlistVisible, menuOpen:!!state.menuOpen},
          menu:{open:menu?.classList.contains('open')||false, display:style(menu)?.display||'', gridTemplateColumns:style(menu)?.gridTemplateColumns||'', text:(menu?.textContent||'').replace(/\\s+/g,' ').trim()},
          groups,
          toggles:toggles.map(el => ({id:el.id,state:el.dataset.state,ariaChecked:el.getAttribute('aria-checked'),title:el.querySelector('.next-settings-toggle-title')?.textContent.trim()||'', hasPill:!!el.querySelector('.next-toggle-pill')})),
          topbar:{rect:rect(topbar), paddingBottom:style(topbar)?.paddingBottom||''},
          layout:{gridTemplateColumns:style(layout)?.gridTemplateColumns||'', rect:rect(layout)},
          media:{rect:rect(media)},
          info:{rect:rect(info)},
          playlist:{display:style(playlist)?.display||'', ariaHidden:playlist?.getAttribute('aria-hidden')||'', rect:rect(playlist)},
          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 screenshot(page, name):
    out = OUT / name
    await page.screenshot(path=str(out), full_page=False)
    return f"qa_screenshots/{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:
            # Mobile menu open screenshot.
            ctx, page = await setup_page(browser, html, 390, 844)
            try:
                await render_active_pass(page, current=4)
                await page.evaluate("state.menuOpen=true; renderControls();")
                await page.wait_for_timeout(250)
                results['mobile_menu'] = await metrics(page)
                results['mobile_menu']['screenshot'] = await screenshot(page, 'rc119_settings_menu_mobile_390x844.png')
            finally:
                await ctx.close()


            # iPad menu open screenshot.
            ctx, page = await setup_page(browser, html, 820, 1180)
            try:
                await render_active_pass(page, current=4)
                await page.evaluate("state.menuOpen=true; renderControls();")
                await page.wait_for_timeout(250)
                results['ipad_menu'] = await metrics(page)
                results['ipad_menu']['screenshot'] = await screenshot(page, 'rc119_settings_menu_ipad_820x1180.png')
            finally:
                await ctx.close()

            # Desktop default menu open.
            ctx, page = await setup_page(browser, html, 1440, 1024)
            try:
                await render_active_pass(page, current=4)
                default = await metrics(page)
                await page.evaluate("state.menuOpen=true; renderControls();")
                await page.wait_for_timeout(250)
                results['desktop_menu'] = await metrics(page)
                results['desktop_menu']['screenshot'] = await screenshot(page, 'rc119_settings_menu_desktop_1440x1024.png')
                # TV mode verification.
                await page.evaluate("state.menuOpen=false; toggleTvMode();")
                await page.wait_for_timeout(250)
                results['desktop_tv'] = await metrics(page)
                results['desktop_tv']['baselineMediaWidth'] = default['media']['rect']['width'] if default.get('media',{}).get('rect') else None
                results['desktop_tv']['screenshot'] = await screenshot(page, 'rc119_tv_mode_desktop_1440x1024.png')
            finally:
                await ctx.close()

            # Live mode verification in separate clean page.
            ctx, page = await setup_page(browser, html, 1440, 1024)
            try:
                await render_active_pass(page, current=4)
                baseline = await metrics(page)
                await page.evaluate("toggleLiveMode();")
                await page.wait_for_timeout(250)
                live = await metrics(page)
                live['baselineTopbarHeight'] = baseline['topbar']['rect']['height'] if baseline.get('topbar',{}).get('rect') else None
                results['desktop_live'] = live
                results['desktop_live']['screenshot'] = await screenshot(page, 'rc119_live_mode_desktop_1440x1024.png')
            finally:
                await ctx.close()
        finally:
            await browser.close()

    errors = []
    # Generic menu checks.
    for key in ('mobile_menu', 'ipad_menu', 'desktop_menu'):
        m = results[key]
        if m.get('brokenImages'):
            errors.append(f"{key}: broken images {m['brokenImages']}")
        if not m['menu']['open'] or m['menu']['display'] == 'none':
            errors.append(f"{key}: menu not visibly open")
        group_keys = {g['key'] for g in m['groups']}
        if not {'view','flow','media','session'}.issubset(group_keys):
            errors.append(f"{key}: missing settings groups {group_keys}")
        if len(m['toggles']) < 8 or any(not t['hasPill'] for t in m['toggles']):
            errors.append(f"{key}: modern toggle pills missing {m['toggles']}")
        if any(x in m['menu']['text'] for x in ['TVAv','LiveAv','ListaPå','AutoPå','FilmPå']):
            errors.append(f"{key}: legacy concatenated labels remain: {m['menu']['text']}")
    tv = results['desktop_tv']
    if not tv['body']['tv'] or not tv['state']['tv']:
        errors.append('desktop_tv: TV body/state not enabled')
    if not tv['body']['playlistHidden'] or tv['playlist']['display'] != 'none' or tv['playlist']['ariaHidden'] != 'true':
        errors.append(f"desktop_tv: playlist not hidden {tv['playlist']}")
    if tv['media']['rect'] and tv['info']['rect'] and not (tv['media']['rect']['width'] > tv['info']['rect']['width'] * 1.25):
        errors.append(f"desktop_tv: media is not clearly prioritized media={tv['media']['rect']['width']} info={tv['info']['rect']['width']}")
    live = results['desktop_live']
    if not live['body']['live'] or not live['state']['live']:
        errors.append('desktop_live: Live body/state not enabled')
    if not live['topbar']['paddingBottom'].startswith('6'):
        errors.append(f"desktop_live: live topbar padding not applied {live['topbar']['paddingBottom']}")
    payload = {"ok": not errors, "method":"real preview DOM via page.set_content; verifies grouped settings menu, visible TV/Live effects, and standardized runtime shell", "results": results, "errors": errors}
    (OUT / 'rc119_legacy_cleanup_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": [v.get('screenshot') for v in results.values() if v.get('screenshot')]}, ensure_ascii=False))


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