#!/usr/bin/env python3
from __future__ import annotations
import json
from pathlib import Path
from bs4 import BeautifulSoup
from playwright.sync_api import sync_playwright

ROOT=Path(__file__).resolve().parents[1]
HTML=(ROOT/'standalone.html').read_text(encoding='utf-8')
EDITION=json.loads((ROOT/'edition.json').read_text())
PALETTE=EDITION['palettes'][0]
APPEARANCE=EDITION['appearances'][0]
VIEWPORTS={'desktop':{'width':1440,'height':1000},'tablet':{'width':834,'height':1112},'mobile':{'width':390,'height':844},'minimum':{'width':320,'height':720},'landscape':{'width':844,'height':390}}
errors=[]
results={'edition':EDITION['displayName'],'palette':PALETTE,'appearance':APPEARANCE,'viewports':{},'interactions':{},'routes':{},'errors':errors}

def fail(msg): errors.append(msg);print('FAIL:',msg,flush=True)
def click(page,selector): page.locator(selector).first.click(timeout=5000);page.wait_for_timeout(70)
def make_page(browser,viewport):
    page=browser.new_page(viewport=viewport,device_scale_factor=1)
    page.emulate_media(reduced_motion='reduce')
    page.set_default_timeout(5000)
    console=[]
    page.on('console',lambda m: console.append(m.text) if m.type=='error' else None)
    page.on('pageerror',lambda e: console.append(str(e)))
    page.add_init_script("window.print=()=>{window.__printCalls=(window.__printCalls||0)+1};Object.defineProperty(navigator,'share',{value:undefined,configurable:true});")
    page.set_content(HTML,wait_until='load',timeout=120000)
    page.evaluate("window.print=()=>{window.__printCalls=(window.__printCalls||0)+1};document.querySelectorAll('.reveal').forEach(el=>el.classList.add('is-visible'))")
    page.wait_for_timeout(120)
    return page,console

with sync_playwright() as p:
    browser=p.chromium.launch(headless=True,executable_path='/usr/bin/chromium',args=['--no-sandbox'])
    for name,viewport in VIEWPORTS.items():
        rows=[]
        for locale in ('en','ja'):
            page,console=make_page(browser,viewport)
            if locale=='ja': click(page,'[data-locale-toggle]')
            metrics=page.evaluate("""()=>{const r=document.documentElement,b=document.body,p=document.querySelector('.preference-controls')?.getBoundingClientRect(),h=document.querySelector('.header-inner')?.getBoundingClientRect();return {palette:r.dataset.palette,appearance:r.dataset.appearance,edition:r.dataset.themeEdition,lang:r.lang,overflow:Math.max(r.scrollWidth,b.scrollWidth)-r.clientWidth,paletteControls:document.querySelectorAll('[data-palette-toggle],[data-palette-option]').length,appearanceControls:document.querySelectorAll('[data-appearance-toggle]').length,localeControls:document.querySelectorAll('[data-locale-toggle]').length,controlsRight:!!p&&!!h&&Math.abs(h.right-p.right)<5,postCount:document.querySelectorAll('[data-post-grid] article').length,mainDisplay:getComputedStyle(document.querySelector('main')).display,buttonHeight:document.querySelector('.button.primary')?.getBoundingClientRect().height||0}}""")
            ok=(metrics['palette']==PALETTE and metrics['appearance']==APPEARANCE and metrics['edition']==f'{PALETTE}-{APPEARANCE}' and metrics['lang']==('ja' if locale=='ja' else 'en-GB') and metrics['overflow']<=1 and metrics['paletteControls']==0 and metrics['appearanceControls']==0 and metrics['localeControls']==1 and metrics['controlsRight'] and metrics['postCount']>=3 and metrics['mainDisplay']!='none' and metrics['buttonHeight']>=36 and not console)
            if not ok: fail(f'{name}/{locale}: {metrics}; console={console}')
            rows.append({'locale':locale,'passed':ok,'overflow':metrics['overflow']})
            if name in ('desktop','mobile') and locale=='en':
                preview=ROOT/'preview';preview.mkdir(exist_ok=True)
                page.screenshot(path=str(preview/f'{name}.png'),full_page=True)
            page.close()
        results['viewports'][name]={'states':rows,'passed':sum(1 for x in rows if x['passed']),'total':len(rows)}

    page,console=make_page(browser,VIEWPORTS['desktop'])
    checks={}
    before=page.locator('[data-metric="leads"]').first.text_content();click(page,'[data-range="90"]');checks['dashboardRange']=before!=page.locator('[data-metric="leads"]').first.text_content()
    click(page,'[data-dashboard-inspect]');checks['dashboardInspector']=page.locator('[data-dashboard-inspect]').get_attribute('aria-expanded')=='true';click(page,'[data-dashboard-inspect]')
    code=page.locator('[data-project-code]').text_content();click(page,'[data-project-index="1"]');page.wait_for_timeout(350);checks['projectSelector']=code!=page.locator('[data-project-code]').text_content()
    click(page,'[data-project-details]');checks['projectDialog']=page.locator('[data-details-dialog]').get_attribute('open') is not None;click(page,'[data-dialog-close]')
    click(page,'[data-project-lightbox]');click(page,'[data-zoom-in]');checks['lightbox']=page.locator('[data-zoom-output]').text_content()!='100%';click(page,'[data-lightbox-close]')
    click(page,'[data-post-open]');checks['postReader']=page.locator('[data-post-dialog]').get_attribute('open') is not None;click(page,'[data-post-share]');checks['share']=page.locator('[data-share-dialog]').get_attribute('open') is not None;click(page,'[data-share-close]')
    click(page,'[data-comment-compose]');page.locator('#comment-name').fill('QA Reader');page.locator('#comment-body').fill('A fixed-theme quality assurance comment.');page.locator('[data-comment-form] input[name="consent"]').check();click(page,'[data-comment-form] button[type="submit"]');page.wait_for_timeout(350);checks['comments']=int(page.locator('[data-comment-count]').text_content() or '0')>=1;click(page,'[data-post-close]')
    click(page,'[data-form-open="project"]');checks['form']=page.locator('[data-form-dialog]').get_attribute('open') is not None;click(page,'[data-form-close]')
    click(page,'[data-company-open]');checks['company']=page.locator('[data-company-details] dl > div').count()>=6;click(page,'[data-company-close]')
    click(page,'[data-social-open]');checks['social']=page.locator('[data-social-list] a').count()>=3;click(page,'[data-social-close]')
    checks['attribution']=page.locator('.design-credit a').get_attribute('href')=='https://iusmusic.com'
    checks['fixedControls']=page.locator('[data-palette-toggle],[data-appearance-toggle]').count()==0
    checks['console']=not console
    for key,value in checks.items():
        if not value: fail(f'Interaction failed: {key}; console={console}')
    results['interactions']=checks
    page.close();browser.close()

# Canonical routes are checked statically because Chromium in this workspace blocks localhost navigation.
for route in ('posts/designing-for-depth/index.html','ja/posts/designing-for-depth/index.html'):
    path=ROOT/'dist'/route
    if not path.exists():
        fail(f'Missing route: {route}');results['routes'][route]={'passed':False};continue
    soup=BeautifulSoup(path.read_text(encoding='utf-8'),'html.parser')
    html=soup.html
    row={'palette':html.get('data-palette'),'appearance':html.get('data-appearance'),'themeEdition':html.get('data-theme-edition'),'paletteControls':len(soup.select('[data-palette-toggle]')),'appearanceControls':len(soup.select('[data-appearance-toggle]')),'article':bool(soup.select_one('[data-direct-article]')),'languageControl':bool(soup.select_one('.language-button'))}
    ok=row['palette']==PALETTE and row['appearance']==APPEARANCE and row['themeEdition']==f'{PALETTE}-{APPEARANCE}' and row['paletteControls']==0 and row['appearanceControls']==0 and row['article'] and row['languageControl']
    if not ok: fail(f'Route {route}: {row}')
    results['routes'][route]={'passed':ok,**row}

preview=ROOT/'preview';preview.mkdir(exist_ok=True)
(preview/'qa-report.json').write_text(json.dumps(results,indent=2,ensure_ascii=False))
view_total=sum(v['total'] for v in results['viewports'].values());view_pass=sum(v['passed'] for v in results['viewports'].values())
md=f'''# QA Report\n\nEdition: **{EDITION['displayName']}**  \nFixed palette: **{PALETTE}**  \nFixed appearance: **{APPEARANCE}**\n\n- Responsive states: {view_pass}/{view_total}\n- Interactions: {sum(bool(v) for v in results['interactions'].values())}/{len(results['interactions'])}\n- Canonical routes: {sum(1 for v in results['routes'].values() if v['passed'])}/{len(results['routes'])}\n- Errors: {len(errors)}\n\nThe package contains no palette selector or appearance switch. The English/Japanese control remains available and right-aligned.\n'''
if errors: md+='\n## Failures\n'+'\n'.join(f'- {e}' for e in errors)+'\n'
(preview/'QA.md').write_text(md)
print(md)
if errors: raise SystemExit(1)
