# 제관 재질 업그레이드 게이트 (2026-07-20)
# 1) fab.html 로드 → JS 예외 0, FAB_MAT 존재
# 2) CP300(통짜) loadOBJ → 재질 metalness 확인, 엣지 라인 자식 존재, 전체복제 비어있지 않음(회귀 방지)
# 3) ROCK_SAW 블럭 → 부품 6개 색이 서로 다름, 엣지 존재(대면수 mesh 생략 확인), 스크린샷
# 4) 클릭선택 하이라이트(emissive) 동작 확인
# 5) 분해 슬라이더 적용 스크린샷
import json, os, sys, subprocess, time
from playwright.sync_api import sync_playwright

D = r"E:\도진팩토리\3D스캔및티칭시스템"
PORT = 8094
URL = "http://localhost:%d/fab.html" % PORT
SHOTS = os.path.join(D, "shots")
os.makedirs(SHOTS, exist_ok=True)

srv = subprocess.Popen([sys.executable, "-m", "http.server", str(PORT)],
                       cwd=D, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
time.sleep(1.5)

R = {"js_exceptions": [], "has_fab_mat": None,
     "obj_mat": None, "obj_edges": None, "obj_whole_copy_meshes": None,
     "block_colors": None, "block_edges": None, "block_distinct": None,
     "highlight_emissive": None}

def _con(m):
    if m.type == "error":
        t = m.text
        if "ERR_CONNECTION_REFUSED" not in t and "Failed to load resource" not in t:
            R["js_exceptions"].append("CONSOLE:" + t)

try:
    with sync_playwright() as p:
        b = p.chromium.launch()
        pg = b.new_page(viewport={"width": 1400, "height": 900})
        pg.on("pageerror", lambda e: R["js_exceptions"].append(str(e)))
        pg.on("console", _con)
        pg.goto(URL)
        pg.wait_for_function("window.FAB && window.FAB.loadOBJ && window.FAB.loadBlock && window.FAB_MAT && window.FAB_COPY", timeout=30000)
        pg.wait_for_timeout(1000)
        R["has_fab_mat"] = pg.evaluate("() => !!window.FAB_MAT && !!window.FAB_MAT.ENV")

        # ── 2) CP300 통짜 ──
        pg.evaluate("async () => { await window.FAB.loadOBJ('fab_models/CP300_frame.obj','CP300'); }")
        pg.wait_for_timeout(1500)  # setTimeout(0) 엣지 추가 대기
        R["obj_mat"] = pg.evaluate("""() => {
            const root = window.FAB_COPY.root; let m=null;
            root.traverse(o=>{ if(o.isMesh && !m) m=o; });
            return { metalness:m.material.metalness, roughness:m.material.roughness,
                     hasEnv:!!m.material.envMap, isStandard:m.material.isMeshStandardMaterial===true,
                     hasEmissive:!!m.material.emissive };
        }""")
        R["obj_edges"] = pg.evaluate("""() => {
            const root = window.FAB_COPY.root; let edges=0, meshes=0;
            root.traverse(o=>{ if(o.isMesh) meshes++; if(o.name==='__fab_edge') edges++; });
            return { meshes, edges };
        }""")
        # 회귀 방지: 통짜 전체복제가 비어있지 않아야
        pg.evaluate("() => window.FAB_COPY.copyWholeBlock()")
        pg.wait_for_timeout(400)
        R["obj_whole_copy_meshes"] = pg.evaluate("() => window.FAB_COPY.meshCount")
        pg.screenshot(path=os.path.join(SHOTS, "fab_mat_cp300.png"))

        # ── 3) ROCK_SAW 블럭 ──
        pg.evaluate("""async () => {
            const r = await fetch('fab_models/parts/index.json',{cache:'no-store'});
            const d = await r.json();
            const blk = d.blocks.find(x=>x.model==='ROCK_SAW_1980');
            await window.FAB.loadBlock(blk.model, blk.parts);
        }""")
        pg.wait_for_timeout(3000)  # 로드 + 엣지 지연
        R["block_colors"] = pg.evaluate("""() => {
            const root = window.FAB_COPY.root; const cols=[];
            root.traverse(o=>{ if(o.isMesh) cols.push(o.material.color.getHexString()); });  // 정본 트리: 중첩 Group → traverse
            return cols;
        }""")
        R["block_edges"] = pg.evaluate("""() => {
            const root = window.FAB_COPY.root; const info=[];
            root.traverse(o=>{ if(o.isMesh){   // 정본 트리: 중첩 Group → traverse
                const faces=o.geometry.attributes.position.count/3;
                let hasEdge=false; o.children.forEach(c=>{ if(c.name==='__fab_edge') hasEdge=true; });
                info.push({name:o.name, faces:Math.round(faces), edge:hasEdge});
            }});
            return info;
        }""")
        cols = R["block_colors"] or []
        R["block_distinct"] = len(set(cols)) if cols else 0
        pg.screenshot(path=os.path.join(SHOTS, "fab_mat_rocksaw.png"))

        # ── 4) 클릭선택 하이라이트: fab_scene selectMode + 부품 emissive 변경 ──
        R["highlight_emissive"] = pg.evaluate("""() => {
            const root = window.FAB_COPY.root; let part=null;
            root.traverse(o=>{ if(o.isMesh && !part) part=o; });  // 정본 트리: 중첩 Group → traverse
            const before = part.material.emissive.getHex();
            // fab_scene 하이라이트와 동일: emissive 세팅이 반영되는지
            part.material.emissive.setHex(0x224a6e);
            const after = part.material.emissive.getHex();
            part.material.emissive.setHex(before);
            return { before, after, restored: part.material.emissive.getHex() };
        }""")

        # ── 5) 분해 슬라이더 적용 스크린샷 ──
        exploded = pg.evaluate("""() => {
            if(!window.FAB_EXP) return 'no FAB_EXP';
            window.FAB_EXP.setOpen(true);
            window.FAB_EXP.applyT(0.8);
            return true;
        }""")
        R["explode_api"] = exploded
        pg.wait_for_timeout(800)
        pg.screenshot(path=os.path.join(SHOTS, "fab_mat_explode.png"))

        b.close()
finally:
    srv.terminate()

om = R["obj_mat"] or {}
oe = R["obj_edges"] or {}
he = R["highlight_emissive"] or {}
ok = (len(R["js_exceptions"]) == 0
      and R["has_fab_mat"] is True
      and om.get("isStandard") is True and om.get("hasEnv") is True
      and abs((om.get("metalness") or 0) - 0.75) < 0.01
      and R["obj_whole_copy_meshes"] and R["obj_whole_copy_meshes"] >= 2
      and (R["block_distinct"] or 0) >= 4
      and he.get("after") == 0x224a6e and he.get("restored") == he.get("before"))

print(json.dumps(R, ensure_ascii=False, indent=1))
print("RESULT:", "PASS" if ok else "FAIL")
sys.exit(0 if ok else 1)
