"""Runs ON gex44 (GPU). usage: spaceships_probe.py <site_url> <out_dir> [slugs comma list]
Loads browse/spaceships.html; per craft: 3/4 view in paint 1, 3/4 in paint 4 (recolour proof), side view, view from below, a
moving parts + engines frame and, when the card has one, the painted copy (alone and with engines); then every family
lineup in random paints with an fps sample (also with moving parts + engines on). <out>/report.json = numbers + errors:
page errors, console warnings, failed requests, frames where the craft is cropped or tiny, buttons without a hover or
focus-visible state."""
import asyncio, json, os, sys
from playwright.async_api import async_playwright
GPU = ['--no-sandbox', '--use-angle=gl-egl', '--use-gl=angle', '--ignore-gpu-blocklist', '--enable-gpu']
FPS = ('new Promise(r => { let n = 0; const t0 = performance.now(); const f = () => { n++; '
       'if (performance.now() - t0 < 2000) requestAnimationFrame(f); else r(n / ((performance.now() - t0) / 1000)); }; requestAnimationFrame(f); })')
FAMILIES = ['fighters', 'alien', 'ships', 'pods', 'cruisers', 'battleships', 'motherships', 'speeders']
STYLE = ('(() => { const s = getComputedStyle(document.activeElement); return {tag: document.activeElement.tagName, '
         'id: (document.activeElement.className || document.activeElement.id) + "|" + (document.activeElement.getAttribute("aria-label") || document.activeElement.textContent.slice(0, 24)), '
         'outline: s.outlineStyle, outlineW: s.outlineWidth, shadow: s.boxShadow}; })()')


async def main(url, out, only=''):
    os.makedirs(out, exist_ok=True)
    rep = {'craft': {}, 'lineups': {}, 'page_errors': [], 'console': [], 'failed_requests': [], 'flags': [], 'ui': {}}
    async with async_playwright() as p:
        b = await p.chromium.launch(args=GPU)
        pg = await b.new_page(viewport={'width': 1440, 'height': 860})
        pg.on('pageerror', lambda e: rep['page_errors'].append(str(e)[:300]))
        pg.on('console', lambda m: m.type in ('error', 'warning') and rep['console'].append(m.text[:300]))
        pg.on('requestfailed', lambda r: rep['failed_requests'].append(r.url[-80:]))
        pg.on('response', lambda r: r.status >= 400 and not r.url.endswith('favicon.ico') and rep['failed_requests'].append(f'{r.status} {r.url[-80:]}'))
        await pg.goto(url + 'spaceships.html')
        await pg.wait_for_function('window.__ready === true', timeout=180000)
        vs = await pg.evaluate('window.__list')
        if not vs:
            rep['flags'].append('spaceships.json has no cards: nothing to probe')
            await pg.screenshot(path=f'{out}/page.png')
            await b.close()
            json.dump(rep, open(f'{out}/report.json', 'w'), indent=1)
            print(json.dumps({'n': 0, 'page_errors': rep['page_errors'], 'console': rep['console'], 'flags': rep['flags']}, indent=1))
            return
        SCROLL = "(() => { const e = document.scrollingElement; return {scrollTop: e.scrollTop, scrollHeight: e.scrollHeight, clientHeight: e.clientHeight, scrollWidth: e.scrollWidth, clientWidth: e.clientWidth}; })()"
        rep['ui']['scroll_start'] = await pg.evaluate(SCROLL)
        want = set(filter(None, only.split(',')))
        shot = lambda path: pg.locator('#c').screenshot(path=path, type='jpeg', quality=88)

        async def snap(v, name, opt):
            r = await pg.evaluate(f"window.__show({json.dumps(v['id'])}, {json.dumps(opt)})")
            fit = await pg.evaluate('window.__fit()')
            await pg.wait_for_timeout(350)
            await shot(f"{out}/{v['slug']}-{name}.jpg")
            r['fit'] = fit
            if not r.get('ok'):
                rep['flags'].append(f"{v['slug']} {name}: show failed {r.get('error')}")
            if fit['cropped']:
                rep['flags'].append(f"{v['slug']} {name}: model cropped by the frame")
            if fit['cover'] < 0.3:
                rep['flags'].append(f"{v['slug']} {name}: model tiny in the frame (cover {fit['cover']})")
            return r

        async def toggle(sel, on):  # click a toggle only when it is enabled and not already in the wanted state
            st = await pg.evaluate(f"(() => {{ const b = document.querySelector('{sel}'); return [b.disabled, b.getAttribute('aria-pressed') === 'true']; }})()")
            if st[0] or st[1] == on:
                return not st[0]
            await pg.click(sel)
            return True

        for v in vs:
            if want and v['slug'] not in want:
                continue
            r = {}
            lum = [0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2] for c in v['paints']] or [0]
            for name, opt in (('a', {'paint': 0}), ('b', {'paint': 3}), ('side', {'paint': 1, 'view': 'side'}),
                              ('below', {'paint': 2, 'view': 'below'}),
                              ('dark', {'paint': lum.index(min(lum)), 'view': 'rear'})):  # the darkest paint against the dark sky
                r[name] = await snap(v, name, opt)
            await pg.evaluate(f"window.__show({json.dumps(v['id'])}, {{paint: 0}})")
            can_move = await toggle('#moving', True)
            can_engine = await toggle('#engines', True)
            await pg.evaluate('window.__freeze(2.6)')
            await pg.wait_for_timeout(500)
            await shot(f"{out}/{v['slug']}-moving.jpg")
            r['live'] = {'can_move': can_move, 'can_engine': can_engine, 'info': await pg.evaluate('window.__info()')}
            await toggle('#moving', False); await toggle('#engines', False)
            await pg.evaluate('window.__freeze(null)')
            if v.get('painted_glb'):
                r['painted'] = await snap(v, 'painted', {'paint': 0, 'painted': True})
                r['painted']['info'] = await pg.evaluate('window.__info()')
                r['painted']['painted_button_visible'] = await pg.evaluate("!document.getElementById('painted').hidden")
                await toggle('#engines', True)
                await pg.wait_for_timeout(400)
                await shot(f"{out}/{v['slug']}-painted-engines.jpg")
                await toggle('#engines', False)
            else:
                r['painted_button_hidden'] = await pg.evaluate("document.getElementById('painted').hidden")
            rep['craft'][v['slug']] = r
        if not want:
            fams = [f for f in FAMILIES if any(v['family'] == f for v in vs)] + sorted({v['family'] for v in vs} - set(FAMILIES))
            for fam in fams:
                lu = await pg.evaluate(f"window.__lineup('{fam}', 7)")
                await pg.wait_for_timeout(800)
                lu['fit'] = await pg.evaluate('window.__fit()')
                lu['fps'] = round(await pg.evaluate(FPS), 1)
                await shot(f'{out}/lineup-{fam}.jpg')
                if lu['fit']['cropped']:
                    rep['flags'].append(f'lineup {fam}: cropped')
                rep['lineups'][fam] = lu
            big = max(rep['lineups'], key=lambda f: rep['lineups'][f].get('tris', 0)) if rep['lineups'] else None
            if big:  # the same lineup with every engine glowing and every part moving
                await pg.evaluate(f"window.__lineup('{big}', 7)")
                await toggle('#moving', True); await toggle('#engines', True)
                await pg.evaluate('window.__freeze(null)')
                await pg.wait_for_timeout(2500)
                rep['lineups'][big]['fps_live'] = round(await pg.evaluate(FPS), 1)
                await shot(f'{out}/lineup-{big}-live.jpg')
                await toggle('#moving', False); await toggle('#engines', False)
            # interface checks on the first craft: hover and focus-visible states, view buttons, nav, text
            await pg.evaluate(f"window.__show({json.dumps(vs[0]['id'])}, {{}})")
            hov = {}
            for name, sel in (('nav', 'nav a:not([aria-current])'), ('pick', '.pick:not([aria-current=true])'), ('btn', '.btn:not(:disabled):not([aria-pressed=true])'),
                              ('swatch', '.sw:not(:disabled):not([aria-pressed=true])'), ('view', '#views .btn')):
                el = pg.locator(sel).first
                css = "(e) => { const s = getComputedStyle(e); return [s.backgroundColor, s.color, s.boxShadow]; }"
                before = await el.evaluate(css)
                await el.hover(); await pg.wait_for_timeout(450)
                after = await el.evaluate(css)
                hov[name] = {'before': before, 'after': after, 'changed': before != after, 'text_colour_kept': before[1] == after[1]}
            await pg.mouse.move(5, 5)
            rep['ui']['hover'] = hov
            await pg.evaluate('document.activeElement.blur()')
            focus, seen = [], []
            for _ in range(120):  # Tab through every control: each must show a focus ring (outline or box-shadow)
                await pg.keyboard.press('Tab')
                st = await pg.evaluate(STYLE)
                key = f"{st['tag']}:{st['id']}"
                if key in seen:  # back at the start of the page
                    break
                seen.append(key)
                if st['tag'] != 'BODY' and st['outline'] == 'none' and st['shadow'] == 'none':
                    focus.append(key)
            rep['ui']['focus_without_ring'] = focus
            rep['ui']['focus_elements_seen'] = len(seen)
            rep['ui']['focus_order'] = seen[:40]
            rep['ui']['scroll_after_tab'] = await pg.evaluate(SCROLL)
            rep['ui']['nav'] = await pg.evaluate("[...document.querySelectorAll('nav a')].map(a => [a.textContent, a.getAttribute('href'), a.getAttribute('aria-current')])")
            rep['ui']['em_dash_in_page'] = await pg.evaluate("document.documentElement.outerHTML.includes('\\u2014')")
            rep['ui']['swatch_labels'] = await pg.evaluate("[...document.querySelectorAll('.sw')].map(b => [b.title, b.getAttribute('aria-label')])")
            rep['ui']['list_groups'] = await pg.evaluate("[...document.querySelectorAll('#list h2')].map(h => h.textContent)")
            rep['ui']['list_first_item'] = await pg.evaluate("document.querySelector('.pick').innerText")
            await pg.screenshot(path=f'{out}/page.png')
            await pg.set_viewport_size({'width': 390, 'height': 844})
            await pg.wait_for_timeout(600)
            await pg.screenshot(path=f'{out}/page-mobile.png')
            await pg.set_viewport_size({'width': 1440, 'height': 860})
            # deep link
            slug = vs[-1]['slug']
            await pg.goto(url + f'spaceships.html?v={slug}')
            await pg.wait_for_function('window.__ready === true', timeout=60000)
            rep['ui']['deep_link'] = await pg.evaluate(f"document.querySelector('.pick[aria-current=true]').innerText.split('\\n')[0]")
            rep['ui']['deep_link_expected'] = vs[-1]['title']
        await b.close()
    json.dump(rep, open(f'{out}/report.json', 'w'), indent=1)
    print(json.dumps({'n': len(rep['craft']), 'page_errors': rep['page_errors'][:5], 'console': rep['console'][:8], 'failed_requests': rep['failed_requests'][:5],
                      'flags': rep['flags'][:20], 'lineups': {k: {'n': x.get('n'), 'fps': x.get('fps'), 'fps_live': x.get('fps_live')} for k, x in rep['lineups'].items()},
                      'ui': {k: rep['ui'].get(k) for k in ('focus_without_ring', 'focus_elements_seen', 'em_dash_in_page', 'deep_link', 'deep_link_expected')}}, indent=1))


asyncio.run(main(*sys.argv[1:4]))
