# dzw_ik_rescue.js(블럭#7 IK 구조대) 검증
# (a) 무변경 보장: 구조대 없이 로드한 대조 페이지와 성공점 IK 결과 JSON(전체 정밀도) 완전 일치
# (b) 구조 동작: 원본 실패점 → 다중시드 재시도로 구조 성공 사례 확보 (rescued:true)
# (c) 기존 ?selftest PASS 유지
# 사전: 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

# 페이지 내에서 목표점 세트를 만들어 solveIK를 직접 호출하는 공통 JS
PROBE_JS = """() => {
    const app=window.__dzw, E=window.DZW5_ENGINE;
    const WIRE=[app.tcpOffset[0],app.tcpOffset[1],app.tcpOffset[2]+(app.WIRE_STICKOUT_MM||15)];
    const HOME={S:-0.1143,L:-63.8005,U:-36.7317,R:0.0025,B:-90.9785,T:-5.5336};
    const opts={base:app.calcBase, tol:0.3};
    // 정상점 5개: 홈 근처 자세들의 FK 점 (확실히 도달 가능)
    const okPoses=[HOME, {...HOME,S:20}, {...HOME,S:-25,L:-50}, {...HOME,U:-20}, {...HOME,S:40,U:-50,B:-70}];
    const okResults=okPoses.map(p=>{
        const t=E.fk(p, app.urdfCalc, WIRE, app.calcBase);
        const r=E.solveIK({x:t.x,y:t.y,z:t.z}, app.urdfCalc, WIRE, HOME, opts);
        return {target:[t.x,t.y,t.z], json:JSON.stringify(r), converged:!!(r&&r.converged), rescued:!!(r&&r.rescued)};
    });
    // 경계점 후보: 극단 자세 FK점 + 살짝 바깥으로 민 점들을 "적대적 시드"로 호출 → 원본 실패 유도
    const extremes=[
        {S:0,L:150,U:-80,R:0,B:85,T:0}, {S:150,L:150,U:-80,R:140,B:-130,T:180},
        {S:0,L:-100,U:155,R:0,B:-130,T:0}, {S:90,L:140,U:-60,R:100,B:80,T:-200},
        {S:-120,L:120,U:-86,R:-140,B:-100,T:150}, {S:0,L:155,U:-86,R:0,B:90,T:0},
        {S:45,L:-90,U:150,R:150,B:-135,T:210}, {S:180-170,L:100,U:-40,R:0,B:-135,T:0}
    ];
    const badSeeds=[{S:0,L:0,U:0,R:0,B:0,T:0}, HOME, {S:0,L:-105,U:160,R:150,B:90,T:210}];
    const edge=[];
    for(const ex of extremes){
        const t=E.fk(ex, app.urdfCalc, WIRE, app.calcBase);
        for(const scale of [1.0, 1.02, 1.05]){
            const tx={x:t.x*scale, y:t.y*scale, z:t.z}; // 수평 방향으로 바깥 확장
            for(const sd of badSeeds){
                const r=E.solveIK(tx, app.urdfCalc, WIRE, sd, opts);
                edge.push({target:[tx.x,tx.y,tx.z], seed:sd, converged:!!(r&&r.converged),
                           rescued:!!(r&&r.rescued), rescueSeed:r?r.rescueSeed:null,
                           err:r?+r.err_mm.toFixed(3):null});
            }
        }
    }
    const stats=window.DZW_IK_RESCUE?window.DZW_IK_RESCUE.stats():null;
    return {okResults, edge, stats};
}"""

async def load_page(ctx, url, errors, block_rescue=False):
    pg = await ctx.new_page()
    await pg.set_viewport_size({'width':1400,'height':900})
    pg.on('console', lambda m: errors.append('[console.error] '+m.text) if (m.type=='error' and not is_noise(m.text)) else None)
    pg.on('pageerror', lambda e: errors.append('[pageerror] '+str(e)))
    if block_rescue:
        await pg.route('**/dzw_ik_rescue.js', lambda route: route.abort())
    await pg.goto(url)
    await pg.wait_for_timeout(16000)
    return pg

async def main():
    URL='http://localhost:8090/DOZIKWORKS_OS_v5_locked.dc.html'
    async with async_playwright() as p:
        br = await p.chromium.launch(headless=True)

        # ── 1. 구조대 포함 페이지 로드 ──
        errA=[]
        ctxA = await br.new_context(); pgA = await load_page(ctxA, URL, errA)
        installed = await pgA.evaluate("() => !!(window.DZW5_ENGINE && DZW5_ENGINE.solveIK && DZW5_ENGINE.solveIK.__dzwIkRescue)")
        print('=== 1. 로드 콘솔에러:', len(errA), '/ 구조대 설치:', installed)
        for e in errA: print('   ', e[:200])

        resA = await pgA.evaluate(PROBE_JS)

        # ── 2. 대조 페이지 (구조대 차단) — 동일 입력 ──
        errB=[]
        ctxB = await br.new_context(); pgB = await load_page(ctxB, URL, errB, block_rescue=True)
        not_installed = await pgB.evaluate("() => !(window.DZW5_ENGINE && DZW5_ENGINE.solveIK && DZW5_ENGINE.solveIK.__dzwIkRescue)")
        resB = await pgB.evaluate(PROBE_JS)
        print('=== 2. 대조페이지(구조대 없음):', not_installed)

        # (a) 성공점 무변경: JSON 전체 정밀도 비트단위 비교
        same = all(a['json']==b['json'] for a,b in zip(resA['okResults'], resB['okResults']))
        all_ok = all(a['converged'] and not a['rescued'] for a in resA['okResults'])
        print('=== 3.(a) 성공점', len(resA['okResults']), '개 — 래핑 전후 결과 완전일치:', same, '/ 전부 converged·rescued없음:', all_ok)

        # (b) 구조 사례: 대조페이지 실패(converged=False) & 구조대페이지 rescued=True인 동일 입력
        rescued_cases = [ (a,b) for a,b in zip(resA['edge'], resB['edge'])
                          if a['rescued'] and a['converged'] and not b['converged'] ]
        orig_fail_cnt = sum(1 for b in resB['edge'] if not b['converged'])
        print('=== 4.(b) 경계 시도', len(resA['edge']), '건 중 원본실패', orig_fail_cnt, '건 / 구조성공', len(rescued_cases), '건')
        for a,_ in rescued_cases[:5]:
            print('    구조: 점(%.0f,%.0f,%.0f) 시드%s 오차 %smm' % (a['target'][0],a['target'][1],a['target'][2],a['rescueSeed'],a['err']))
        print('    통계 API:', json.dumps(resA['stats']))

        await ctxA.close(); await ctxB.close()

        # ── (c) 셀프테스트 ──
        errC=[]; selftest_line=[None]
        ctxC = await br.new_context(); pgC = await ctxC.new_page()
        pgC.on('console', lambda m: selftest_line.__setitem__(0, m.text) if '[셀프테스트]' in m.text and 'PASS' in m.text else None)
        await pgC.goto(URL+'?selftest')
        await pgC.wait_for_timeout(25000)
        st = await pgC.evaluate("() => { const d=document.getElementById('dzw-selftest-result'); return d?d.textContent:null; }")
        fails = -1
        if st:
            try: fails = len(json.loads(st).get('fail',[]))
            except Exception: fails = -1
        if fails < 0 and selftest_line[0] and 'FAIL 0' in selftest_line[0]:
            fails = 0   # DIV가 순수 JSON이 아니면 콘솔 판정선(PASS n / FAIL 0)으로 판정
        print('=== 5.(c) 셀프테스트:', selftest_line[0], '/ FAIL수:', fails)
        await ctxC.close()

        ok = (len(errA)==0 and installed and same and all_ok and fails==0
              and (len(rescued_cases)>=1 or orig_fail_cnt==0))
        if len(rescued_cases)==0 and orig_fail_cnt>0:
            print('※ 정직보고: 원본 실패점이 전부 진짜 도달불가(전 시드 실패)였음 — 구조 성공 사례 미확보')
        print('=== 최종:', 'PASS' if ok else 'FAIL')
        await br.close()

asyncio.run(main())
