# dzw_semantic_path.js(블럭#8 의미 티칭) 검증
# 흐름: 로드(콘솔에러 0) → 구간 등록(안전게이트 테스트와 동일) → segsToJbi
#       → JBI + .meaning.json 둘 다 다운로드 / 레코드 수=이동명령 수 / MOVC에 MOT-02
#       → 게이트 강제 차단 시 의미파일 미생성 확인
# 사전: python dzw_server.py (8090)
import asyncio, json
from playwright.async_api import async_playwright

def is_noise(t): return 'Failed to load resource' in t

async def main():
    errors, noise, downloads = [], [], []
    async with async_playwright() as p:
        br = await p.chromium.launch(headless=True)
        ctx = await br.new_context(accept_downloads=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)))
        dl_bodies = {}
        async def on_dl(d):
            downloads.append(d.suggested_filename)
            try:
                path = await d.path()
                dl_bodies[d.suggested_filename] = open(path, 'r', encoding='utf-8', errors='replace').read()
            except Exception: pass
        pg.on('download', lambda d: asyncio.ensure_future(on_dl(d)))
        await pg.goto('http://localhost:8090/DOZIKWORKS_OS_v5_locked.dc.html')
        await pg.wait_for_timeout(16000)
        print('=== 1. 로드. 실질 콘솔에러:', len(errors), '/ 404노이즈:', len(noise))
        for e in errors: print('   ', e[:200])

        inst = await pg.evaluate("""() => ({
            gate: !!(DZW5_EDGEPATH.segsToJbi.__dzwSafetyGate),
            sem:  !!(DZW5_EDGEPATH.segsToJbi.__dzwSemantic) })""")
        print('=== 2. 설치상태 (게이트/의미):', inst)

        setup = await pg.evaluate("""() => {
            const app=window.__dzw, EP=window.DZW5_EDGEPATH, E=window.DZW5_ENGINE, T=app._T, km=app._knuckleMesh;
            if(!app||!km) return {ok:false, why:'앱 미초기화'};
            app._faceMode=1;
            const regA=EP._selectAutoRegion(km,14047), regB=EP._selectAutoRegion(km,10023);
            app._faceARegion=new Set(regA); app._faceBRegion=new Set(regB);
            const sb=E.sharedBoundary(km,app._faceARegion,app._faceBRegion);
            if(!sb.length) return {ok:false, why:'공유경계 0'};
            EP._registerSeamAsPath(app,sb);
            const seg=app._teachSegments[app._teachSegments.length-1];
            km.updateWorldMatrix(true,true);
            const prj=(arr)=>{ const v=new T.Vector3(...arr); km.localToWorld(v); v.project(app.three.cam); return {mx:v.x,my:v.y}; };
            const evt={button:0,clientX:500,clientY:400,pointerId:1,shiftKey:false};
            const n0=prj(seg.pts[2]), n1=prj(seg.pts[Math.min(seg.pts.length-1,80)]);
            EP.startArcOnMode(app);
            const r1=EP._handleArcClick(app,evt,n0.mx,n0.my,app.three.cam);
            const r2=EP._handleArcClick(app,evt,n1.mx,n1.my,app.three.cam);
            // BOSS 태그 지정 (localStorage) — DES-04 부착 확인용
            const segNo=app._arcSegPts[0].no;
            localStorage.setItem('dzw_semantic_tags', JSON.stringify({[segNo]:'BOSS모서리'}));
            return {ok:r1===true&&r2===true&&app._arcSegPts.length===1, segNo};
        }""")
        print('=== 3. 구간 등록:', json.dumps(setup, ensure_ascii=False))

        await pg.evaluate("() => { DZW5_EDGEPATH.segsToJbi(window.__dzw); }")
        await pg.wait_for_timeout(4000)
        sem = await pg.evaluate("() => window.__dzwSemanticLast || null")
        gate = await pg.evaluate("() => window.__dzwSafetyGateLast ? window.__dzwSafetyGateLast.blocked : null")
        print('=== 4. 게이트 blocked:', gate, '/ 의미 요약:', json.dumps(sem, ensure_ascii=False))
        print('=== 5. 다운로드:', downloads)
        jbi = [f for f in downloads if f.upper().endswith('.JBI')]
        mean = [f for f in downloads if f.endswith('.meaning.json')]

        ok_counts = ok_movc = ok_tag = False
        if mean and mean[0] in dl_bodies:
            data = json.loads(dl_bodies[mean[0]])
            recs = data['records']
            moves = [r for r in recs if r.get('c')]
            jbi_txt = dl_bodies.get(jbi[0], '')
            n_move_inst = sum(1 for L in jbi_txt.splitlines() if L.startswith('MOV'))
            ok_counts = len(moves) == n_move_inst and n_move_inst > 0
            movc = [r for r in moves if r['inst'].startswith('MOVC')]
            ok_movc = all('MOT-02' in (r.get('rule') or '') for r in movc) if movc else True
            boss = [r for r in moves if 'DES-04' in (r.get('rule') or '')]
            ok_tag = len(boss) > 0
            print('=== 6. 이동레코드', len(moves), '/ JBI MOV명령', n_move_inst,
                  '/ MOVC수', len(movc), 'MOT-02 부착:', ok_movc, '/ BOSS(DES-04) 레코드:', len(boss))
            print('    예시 레코드:', json.dumps(moves[len(moves)//2], ensure_ascii=False))
            unknown_src_bad = [r for r in recs if r['src'] not in
                ('뷰어 생성규칙','공정서 §1','공정서 §1 속도표','공정서 §1·§3','검증 홈자세','자동분류-근거없음')]
            print('    출처 위반(지어낸 출처):', len(unknown_src_bad))

        # ── 7. 게이트 차단 시 의미파일 미생성 ──
        n_dl_before = len(downloads)
        blocked = await pg.evaluate("""() => {
            const E=window.DZW5_ENGINE, orig=E.solveIK;
            E.solveIK=function(){ const r=orig.apply(this,arguments);
                if(r&&r.converged) r.err_mm=99; return r; };   // 오차초과 강제 → 게이트 차단
            window.__dzwSemanticLast=null;
            try{ DZW5_EDGEPATH.segsToJbi(window.__dzw); }finally{ E.solveIK=orig; }
            return window.__dzwSafetyGateLast ? window.__dzwSafetyGateLast.blocked : null;
        }""")
        await pg.wait_for_timeout(2500)
        no_meaning = await pg.evaluate("() => window.__dzwSemanticLast === null")
        new_dl = downloads[n_dl_before:]
        print('=== 7. 강제차단 blocked:', blocked, '/ 신규 다운로드:', new_dl, '/ 의미파일 미생성:', no_meaning)

        ok = (len(errors)==0 and inst['gate'] and inst['sem'] and setup.get('ok')
              and gate is False and len(jbi)==1 and len(mean)==1
              and ok_counts and ok_movc and ok_tag
              and blocked is True and len(new_dl)==0 and no_meaning)
        print('=== 최종:', 'PASS' if ok else 'FAIL')
        await br.close()

asyncio.run(main())
