# dzw_ar.js — AR 진입 + 돋보기 조준 + 캔버스 정렬 검증
# 크로미움 가짜 카메라(--use-fake-device-for-media-stream)로 실제 getUserMedia 경로를 태운다.
# 확인: AR 켜짐 / 영상 스트림 / 돋보기 표시·추종 / 8점 수집 / phase 전이 / 캔버스가 영상 박스에 포개짐
# 사전: python dzw_server.py (8090) 실행 상태
# 태블릿 실경로(HTTPS) 검증: python _ar_loupe_test.py https://localhost:8443/... (dzw_phone_server.py 필요)
import asyncio, json, sys
from playwright.async_api import async_playwright

URL = sys.argv[1] if len(sys.argv) > 1 else 'http://localhost:8090/DOZIKWORKS_OS_v5_locked.dc.html'

def is_noise(t):
    return 'Failed to load resource' in t

FAKE_CAM = [
    '--use-fake-device-for-media-stream',
    '--use-fake-ui-for-media-stream',
    '--autoplay-policy=no-user-gesture-required',
]

async def main():
    errors, noise = [], []
    async with async_playwright() as p:
        br = await p.chromium.launch(headless=True, args=FAKE_CAM)
        ctx = await br.new_context(permissions=['camera'], ignore_https_errors=True)
        pg = await ctx.new_page()
        await pg.set_viewport_size({'width': 1400, 'height': 900})
        pg.on('console', lambda m: (noise if is_noise(m.text) else errors).append('[console.error] ' + m.text) if m.type == 'error' else None)
        pg.on('pageerror', lambda e: errors.append('[pageerror] ' + str(e)))
        await pg.goto(URL)
        await pg.wait_for_timeout(16000)
        print('=== 0. 주소:', URL)
        print('=== 1. 로드. 실질 콘솔에러:', len(errors), '/ 404노이즈:', len(noise))
        for e in errors: print('   ', e[:200])

        # AR 버튼으로 진입 (콘솔 호출이 아니라 CEO가 실제로 누를 경로)
        await pg.click('#dzw-ar-launch')
        await pg.wait_for_timeout(3000)

        state = await pg.evaluate("""() => {
            const ui=document.getElementById('dzw-ar'), v=document.getElementById('dzw-ar-video');
            return {AR표시: !!ui && ui.style.display==='block',
                    영상스트림: !!(v && v.srcObject),
                    영상크기: v?[v.videoWidth,v.videoHeight]:null,
                    안내문: (document.getElementById('dzw-ar-guide')||{}).textContent||''};
        }""")
        print('=== 2. AR 진입:', json.dumps(state, ensure_ascii=False)[:200])
        if not state['AR표시'] or not state['영상스트림']:
            print('!! AR 미진입 — 중단'); await br.close(); return

        # 돋보기: 누르고 → 끌고 → 뗀다
        box = await pg.evaluate("""() => { const r=document.getElementById('dzw-ar-video').getBoundingClientRect();
                                           return {l:r.left,t:r.top,w:r.width,h:r.height}; }""")
        cx, cy = box['l'] + box['w'] * 0.4, box['t'] + box['h'] * 0.5

        await pg.mouse.move(cx, cy)
        await pg.mouse.down()
        await pg.wait_for_timeout(300)
        during = await pg.evaluate("""() => { const L=document.getElementById('dzw-ar-loupe');
            if(!L) return {표시:false};
            const c=L.querySelector('canvas'), g=c.getContext('2d');
            const d=g.getImageData(0,0,c.width,c.height).data;
            let nonblack=0; for(let i=0;i<d.length;i+=4) if(d[i]+d[i+1]+d[i+2]>30) nonblack++;
            return {표시:L.style.display==='block', 위치:[L.style.left,L.style.top],
                    영상그려짐: nonblack> (c.width*c.height*0.2)};
        }""")
        print('=== 3. 돋보기 (누르는 중):', json.dumps(during, ensure_ascii=False))

        await pg.mouse.move(cx + 60, cy + 40)
        await pg.wait_for_timeout(200)
        moved = await pg.evaluate("""() => { const L=document.getElementById('dzw-ar-loupe');
                                            return {위치:[L.style.left,L.style.top]}; }""")
        await pg.mouse.up()
        await pg.wait_for_timeout(300)
        after = await pg.evaluate("""() => ({
            돋보기숨김: document.getElementById('dzw-ar-loupe').style.display==='none',
            점개수: document.getElementById('dzw-ar-dots').children.length })""")
        print('=== 4. 조준 추종:', json.dumps(moved, ensure_ascii=False), '| 뗀 뒤:', json.dumps(after, ensure_ascii=False))

        follows = during['위치'] != moved['위치']

        # 나머지 7점 찍어 phase 2 전이 확인
        for i in range(7):
            x = box['l'] + box['w'] * (0.15 + 0.1 * i)
            y = box['t'] + box['h'] * (0.25 + 0.06 * i)
            await pg.mouse.move(x, y); await pg.mouse.down(); await pg.wait_for_timeout(60)
            await pg.mouse.up(); await pg.wait_for_timeout(60)

        final = await pg.evaluate("""() => { const ui=document.getElementById('dzw-ar');
            return {찍힌점: document.getElementById('dzw-ar-dots').children.length,
                    영상숨김전환: ui.style.visibility==='hidden',   // [2026-07-18] 반투명→완전숨김 (3D 뿌옇던 문제)
                    안내문: (document.getElementById('dzw-ar-guide')||{}).textContent||''}; }""")
        print('=== 5. 8점 수집:', json.dumps(final, ensure_ascii=False)[:220])

        # 캔버스가 영상이 실제 그려진 박스에 포개지는가 (계산 맞아도 화면 어긋나던 버그)
        align = await pg.evaluate("""() => {
            const v=document.getElementById('dzw-ar-video'), c=document.getElementById('dzw-ar-canvas');
            const r=v.getBoundingClientRect();
            const va=v.videoWidth/v.videoHeight, ea=r.width/r.height;
            let w,h,ox,oy;
            if(ea>va){ h=r.height; w=h*va; ox=(r.width-w)/2; oy=0; } else { w=r.width; h=w/va; ox=0; oy=(r.height-h)/2; }
            const cr=c.getBoundingClientRect();
            return {기대박스:[+ox.toFixed(1),+oy.toFixed(1),+w.toFixed(1),+h.toFixed(1)],
                    실제캔버스:[+(cr.left-r.left).toFixed(1),+(cr.top-r.top).toFixed(1),+cr.width.toFixed(1),+cr.height.toFixed(1)],
                    어긋남px:+Math.max(Math.abs(cr.left-r.left-ox),Math.abs(cr.top-r.top-oy),
                                      Math.abs(cr.width-w),Math.abs(cr.height-h)).toFixed(2)};
        }""")
        print('=== 6. 캔버스↔영상 정렬:', json.dumps(align, ensure_ascii=False))

        ok = (state['AR표시'] and state['영상스트림'] and during['표시'] and during['영상그려짐']
              and follows and after['돋보기숨김'] and after['점개수'] == 1
              and final['찍힌점'] == 8 and final['영상숨김전환'] and align['어긋남px'] < 1.0
              and not errors)
        print()
        print('=== 결과:', 'PASS' if ok else 'FAIL')
        await br.close()

asyncio.run(main())
