#!/usr/bin/env python3
"""v97 RC2 / RC121 Image Admin real-DOM QA.

Verifies the new Bildadmin surface and backend-hooked mock workflow without touching player lifecycle:
- screen renders from actual preview.html, CSS and JS
- templates/status/health fetch paths are used
- prompt is English and editable
- generate/approve buttons call backend contract
- preview uses the v97 layered visual component
"""
from __future__ import annotations
import asyncio, base64, json, mimetypes, re
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 templates={ok:true,version:'qa',defaultTemplateId:'next-athlete-blond-b',templates:[
    {id:'next-athlete-blond-b',label:'NEXT Athlete Blond B',description:'QA primary athlete template',referencePath:'assets/img/athlete-templates/next-athlete-blond-b/reference.webp',thumbPath:'assets/img/athlete-templates/next-athlete-blond-b/thumb.webp',styleProfile:'Use the supplied athlete reference image as identity.',canvasProfile:'Transparent 16:9 WebP figure layer.'},
    {id:'next-athlete-blond-a',label:'NEXT Athlete Blond A',description:'QA alternate athlete template',referencePath:'assets/img/athlete-templates/next-athlete-blond-a/reference.webp',thumbPath:'assets/img/athlete-templates/next-athlete-blond-a/thumb.webp',styleProfile:'Use the supplied athlete reference image as identity.',canvasProfile:'Transparent 16:9 WebP figure layer.'}
  ]};
  const masterPrompts={version:'qa',defaultPromptId:'next-athlete-reference-16x9-v3',prompts:[{id:'next-athlete-reference-16x9-v3',label:'QA prompt',prompt:'Create a premium transparent WebP exercise figure for NEXT Athlete OS. Use the supplied athlete reference image as the visual identity. Create one isolated athlete figure layer on a transparent 1600 x 900 canvas. Exercise motion spec:\\nEXERCISE_MOTION_SPEC'}]};
  const motionPrompts={version:'qa',prompts:{'linear-pogos':{label:'Pogohopp',panelCount:4,prompt:'Show the athlete performing forward pogo hops in four clear phases.'}}};
  let status={ok:true,version:'qa',updatedAt:null,exercises:{}};
  window.__imageAdminCalls=[]; window.__imageAdminFetchUrls=[];
  const nativeFetch = window.fetch ? window.fetch.bind(window) : null;
  window.fetch=function(url, options){
    const s=(typeof url==='string') ? url : ((url && url.url) ? url.url : String(url||'')); window.__imageAdminFetchUrls.push(s);
    if(s.includes('api/image-admin/health.php')) return Promise.resolve({ok:true,json:function(){return Promise.resolve({ok:true,php:'8.qa',curl:true,providerMode:'mock',writable:{status:true,drafts:true,figures:true,variants:true}});}});
    if(s.includes('api/image-admin/templates.php')) return Promise.resolve({ok:true,json:function(){return Promise.resolve(templates);}});
    if(s.includes('api/image-admin/status.php')) return Promise.resolve({ok:true,json:function(){return Promise.resolve(status);}});
    if(s.includes('assets/data/image-admin/master-prompts.json')) return Promise.resolve({ok:true,json:function(){return Promise.resolve(masterPrompts);}});
    if(s.includes('assets/data/image-admin/exercise-motion-prompts.json')) return Promise.resolve({ok:true,json:function(){return Promise.resolve(motionPrompts);}});
    if(s.includes('generate.php')){
      const body=options&&options.body?JSON.parse(options.body):{};
      window.__imageAdminCalls.push({type:'generate',body});
      const key=body.exerciseKey||'exercise';
      const draft={id:'qa_draft_001',exerciseKey:key,templateId:body.templateId||'next-athlete-v1',theme:body.theme||'hockey',url:'assets/img/exercises/figures/_default.webp',prompt:body.prompt||'',mock:true,status:'draft',createdAt:new Date().toISOString()};
      status.exercises[key]={exerciseKey:key,status:'draft',activeVariant:null,variants:[],drafts:[draft]};
      return Promise.resolve({ok:true,json:function(){return Promise.resolve({ok:true,mock:true,draft,entry:status.exercises[key]});}});
    }
    if(s.includes('approve.php')){
      const body=options&&options.body?JSON.parse(options.body):{};
      window.__imageAdminCalls.push({type:'approve',body});
      const key=body.exerciseKey||'exercise';
      const entry=status.exercises[key]||{exerciseKey:key,drafts:[]};
      entry.status='approved'; entry.activeVariant=body.templateId||'next-athlete-v1'; entry.variants=[entry.activeVariant]; entry.activeUrl='assets/img/exercises/figures/'+key+'.webp';
      status.exercises[key]=entry;
      return Promise.resolve({ok:true,json:function(){return Promise.resolve({ok:true,entry});}});
    }
    if(s.includes('ota_youtube_proxy.php')) return Promise.resolve({ok:true,json:function(){return Promise.resolve({embedUrl:'https://www.youtube.com/embed/TESTID12345?enablejsapi=1',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'));
  };
})();
</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):
    await page.wait_for_timeout(250)
    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)
    page=await ctx.new_page(); page.set_default_timeout(12000)
    await page.set_content(html, wait_until="domcontentloaded")
    return ctx,page

async def open_image_admin(page):
    await page.evaluate("""() => { if(typeof showImageAdmin === 'function') showImageAdmin(); }""")
    await page.wait_for_timeout(700)
    await page.evaluate("""() => { const first=document.querySelector('.next-image-admin-row'); first && first.click(); }""")
    await page.wait_for_timeout(300)
    await inline_dom_images(page)

async def run_generate_approve(page):
    await page.evaluate("""() => { Array.from(document.querySelectorAll('.next-image-admin-actions button')).find(b=>/Skapa mock-draft|Generera draft/.test(b.textContent||''))?.click(); }""")
    await page.wait_for_timeout(500)
    await page.evaluate("""() => { Array.from(document.querySelectorAll('.next-image-admin-actions button')).find(b=>/Godkänn/.test(b.textContent||''))?.click(); }""")
    await page.wait_for_timeout(500)
    await inline_dom_images(page)

async def metrics(page):
    return await page.evaluate("""
      () => {
        const prompt=document.getElementById('imageAdminPrompt');
        const selected=document.querySelector('.next-image-admin-row.is-active');
        const visual=document.querySelector('#imageAdminDetail .next-exercise-visual');
        const bg=visual?.querySelector('.next-exercise-visual-bg');
        const fig=visual?.querySelector('.next-exercise-visual-figure');
        const calls=window.__imageAdminCalls||[]; const fetchUrls=window.__imageAdminFetchUrls||[];
        return {
          screenActive:document.getElementById('imageAdminScreen')?.classList.contains('active'),
          hasShell:!!document.querySelector('.next-image-admin-page'),
          hasRows:document.querySelectorAll('.next-image-admin-row').length,
          hasSelected:!!selected,
          selectedKey:selected?.dataset.exerciseKey||'',
          hasPrompt:!!prompt,
          promptEnglish:/Create a premium transparent WebP exercise figure/.test(prompt?.value||''),
          promptMentionsReference:/Athlete reference:|supplied athlete reference image/.test(prompt?.value||''),
          hasMotionSpec:!!document.getElementById('imageAdminMotionSpec'),
          hasHandoffPanel:/ChatGPT-handoff/.test(document.querySelector('#imageAdminDetail')?.textContent||''),
          hasReferenceCard:!!document.querySelector('.next-image-admin-reference-card'),
          hasTemplateSelect:!!document.getElementById('imageAdminTemplate'),
          hasThemeSelect:!!document.getElementById('imageAdminTheme'),
          hasVisual:!!visual,
          hasBg:!!bg,
          hasFigure:!!fig,
          bgSrc:bg?.getAttribute('data-qa-original-src')||bg?.getAttribute('src')||'',
          figureSrc:fig?.getAttribute('data-qa-original-src')||fig?.getAttribute('src')||'',
          fetchUrls,
          generateCalled:calls.some(c=>c.type==='generate'),
          approveCalled:calls.some(c=>c.type==='approve'),
          statusText:document.getElementById('imageAdminActionStatus')?.textContent||'',
          approvedBadge:Array.from(document.querySelectorAll('.next-image-status')).some(el=>/Godkänd/.test(el.textContent||'')),
          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, timeout=12000)
    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:
            for name,w,h in VIEWPORTS:
                ctx,page=await setup_page(browser, html, w, h)
                try:
                    await open_image_admin(page)
                    if name == 'desktop':
                        await run_generate_approve(page)
                    results[name]=await metrics(page)
                    results[name]['screenshot']=await screenshot(page, f"rc121_image_admin_{name}_{w}x{h}.png")
                finally:
                    await ctx.close()
        finally:
            await browser.close()
    errors=[]
    for name,m in results.items():
        for k in ['screenActive','hasShell','hasSelected','hasPrompt','promptEnglish','promptMentionsReference','hasMotionSpec','hasHandoffPanel','hasReferenceCard','hasTemplateSelect','hasThemeSelect','hasVisual','hasBg','hasFigure']:
            if not m.get(k): errors.append(f"{name}: missing/false {k}: {m}")
        if m['hasRows'] <= 10: errors.append(f"{name}: too few rows {m['hasRows']}")
        if 'themes/exercise-backgrounds' not in m['bgSrc']: errors.append(f"{name}: theme bg not used {m['bgSrc']}")
        if '_default.webp' not in m['figureSrc'] and '/figures/' not in m['figureSrc']: errors.append(f"{name}: wrong figure src {m['figureSrc']}")
        if m['brokenImages']: errors.append(f"{name}: broken images {m['brokenImages']}")
    if not results.get('desktop',{}).get('generateCalled'): errors.append('desktop: generate backend contract not called')
    if not results.get('desktop',{}).get('approveCalled'): errors.append('desktop: approve backend contract not called')
    (OUT/'rc121_image_admin_dom_metrics.json').write_text(json.dumps(results,indent=2,ensure_ascii=False),encoding='utf-8')
    if errors:
        raise SystemExit('RC121 image admin real-DOM QA failed:\n'+'\n'.join(errors))
    print('RC121 image admin real-DOM QA OK')

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