# -*- coding: utf-8 -*-
# 실사용 클릭 전수검사 (2026-07-14) — CEO 보고 "판 클릭 색상 안 돌아옴" 재현
# 규칙: 동작은 전부 진짜 마우스 이벤트(page.mouse.*). evaluate는 상태 '읽기'만.
# 실사용 경로: 선택 툴 → 제품 클릭(선택) → 티칭패널 '용접' 탭 → 판A 시작 → 캔버스 클릭
import os, sys, io, json
from playwright.sync_api import sync_playwright
from PIL import Image

URL = "http://localhost:8090/DOZIKWORKS_OS_v5_locked.dc.html"
BASE = os.path.dirname(os.path.abspath(__file__))
OUT = os.path.join(BASE, "audit_260714")
os.makedirs(OUT, exist_ok=True)

logs, results = [], []

def report(name, ok, evidence):
    results.append((name, "PASS" if ok else "FAIL", evidence))
    print(("[PASS] " if ok else "[FAIL] ") + name + " | " + evidence)

CANVAS = {"x": 190, "y": 100, "w": 900, "h": 470}  # 3D 캔버스 영역만 계수 (UI 제외), 런타임에 갱신

def count_colors(png_bytes):
    im = Image.open(io.BytesIO(png_bytes)).convert("RGB")
    px = im.load()
    x0, y0 = int(CANVAS["x"]), int(CANVAS["y"])
    x1, y1 = int(CANVAS["x"] + CANVAS["w"]), int(CANVAS["y"] + CANVAS["h"])
    red = blue = 0
    for y in range(y0, min(y1, im.size[1]), 2):
        for x in range(x0, min(x1, im.size[0]), 2):
            r, g, b = px[x, y]
            if r > 170 and r - g > 50 and r - b > 50: red += 1          # 판A 빨강(살몬 포함)
            elif b > 140 and b - r > 40 and g - r > 20: blue += 1       # 판B 0x0088FF(청록 블렌드)
    return red, blue

def shot(pg, name):
    data = pg.screenshot()
    with open(os.path.join(OUT, name + ".png"), "wb") as f: f.write(data)
    r, b = count_colors(data)
    print(f"  shot {name}: red={r} blue={b}")
    return r, b

def click_el(pg, selector):
    loc = pg.locator(selector).first
    try: box = loc.bounding_box()
    except Exception: return False
    if not box or box["width"] == 0: return False
    pg.mouse.click(box["x"] + box["width"]/2, box["y"] + box["height"]/2)
    return True

def click_text(pg, text, exact=True):
    loc = pg.get_by_text(text, exact=exact)
    for i in range(loc.count()):
        try: box = loc.nth(i).bounding_box()
        except Exception: continue
        if box and box["width"] > 0:
            pg.mouse.click(box["x"] + box["width"]/2, box["y"] + box["height"]/2)
            return True
    return False

READ_MODE = ("() => ({mode: window.__dzw._faceMode, "
             "a: window.__dzw._faceARegion?window.__dzw._faceARegion.size:0, "
             "b: window.__dzw._faceBRegion?window.__dzw._faceBRegion.size:0, "
             "sel: !!window.__dzw._tcSelected, "
             "panel: (document.getElementById('dzw-xform-panel')||{}).style ? document.getElementById('dzw-xform-panel').style.display : 'none'})")

# 캔버스 위 제품 히트 지점 찾기 — 로컬 Raycaster (읽기 전용 계산)
FIND_HITS = """() => {
  const a = window.__dzw;
  const cam = a.three.cam || a.three.camera, dom = a.three.rnd.domElement;
  const rect = dom.getBoundingClientRect();
  const RC = (window.THREE && THREE.Raycaster) ? THREE.Raycaster : a._ray.constructor;
  const ray = new RC();
  const hits = [];
  for (let gy = 0.15; gy <= 0.86; gy += 0.06) {
    for (let gx = 0.15; gx <= 0.86; gx += 0.06) {
      const cx = rect.left + rect.width * gx, cy = rect.top + rect.height * gy;
      const mx = ((cx - rect.left) / rect.width) * 2 - 1;
      const my = -((cy - rect.top) / rect.height) * 2 + 1;
      ray.setFromCamera({x: mx, y: my}, cam);
      const h = ray.intersectObjects([a._knuckleMesh], true);
      if (h.length) {
        const n = h[0].face.normal;
        hits.push({x: cx, y: cy, nx: n.x, ny: n.y, nz: n.z, fi: h[0].faceIndex});
      }
    }
  }
  return hits;
}"""

def pick_two_points(pg):
    hits = pg.evaluate(FIND_HITS)
    if not hits: return None, None
    pA = hits[len(hits)//2]
    pB = None
    for h in hits:
        dot = pA["nx"]*h["nx"] + pA["ny"]*h["ny"] + pA["nz"]*h["nz"]
        if abs(dot) < 0.4:
            pB = h; break
    if pB is None and len(hits) > 1: pB = hits[-1]
    return pA, pB

def joints(pg):
    return pg.evaluate("() => JSON.stringify(window.__dzw.state.joints)")

with sync_playwright() as p:
    br = p.chromium.launch(headless=True)
    pg = br.new_page(viewport={"width": 1600, "height": 900})
    pg.on("console", lambda m: logs.append(f"[{m.type}] {m.text}"))
    pg.on("pageerror", lambda e: logs.append("[pageerror] " + str(e)))
    pg.goto(URL)
    pg.wait_for_function("window.__dzw && window.__dzw._knuckleMesh && window.DZW5_EDGEPATH && window.__dzw.three", timeout=60000)
    pg.wait_for_timeout(5000)  # pick_toolbar 배선 등 안정화

    # 지능 패킷(dzwb) 슬라이드 패널이 자동으로 열려 우측 480px를 덮음 → 사용자처럼 ✕로 닫는다
    dzwb_open = pg.evaluate("() => { const w=document.getElementById('dzwb-wrap'); return !!(w && w.className.includes('shown')); }")
    if dzwb_open:
        click_el(pg, "#dzwb-x"); pg.wait_for_timeout(800)
    dzwb_after = pg.evaluate("() => { const w=document.getElementById('dzwb-wrap'); return !!(w && w.className.includes('shown')); }")
    report("지능패킷 패널 자동열림→✕닫기", not dzwb_after, f"로드시 열림={dzwb_open}, 닫힘성공={not dzwb_after}")

    click_text(pg, "FIT"); pg.wait_for_timeout(1500)
    cv = pg.evaluate("() => { const r = window.__dzw.three.rnd.domElement.getBoundingClientRect(); return {x:r.left, y:r.top, w:r.width, h:r.height}; }")
    CANVAS.update(cv)
    shot(pg, "00_load")

    # ── 실사용 경로: 선택 툴 → 제품 클릭 → 티칭 패널 열림 ──
    ok_sel = click_el(pg, '[data-dzw-pick="select"]'); pg.wait_for_timeout(500)
    pA0, _ = pick_two_points(pg)
    if pA0: pg.mouse.click(pA0["x"], pA0["y"]); pg.wait_for_timeout(1000)
    st = pg.evaluate(READ_MODE)
    report("선택툴→제품클릭→티칭패널", ok_sel and st["sel"] and st["panel"] == "block",
           f"선택됨={st['sel']}, 패널display={st['panel']}")
    shot(pg, "01_panel_open")

    # 정합 탭 클릭 (판A 선택 버튼은 정합 탭에 있음 — dzw_teach_core.js 탭 재구성 후 기준)
    ok_tab = click_el(pg, '.dzw-tp-tab[data-tab="align"]'); pg.wait_for_timeout(500)
    vis = pg.evaluate("() => { const b=document.getElementById('dzw-curve-mode-btn'); const r=b?b.getBoundingClientRect():{width:0}; return r.width>0; }")
    report("정합탭→판A버튼 표시", ok_tab and vis, f"탭클릭={ok_tab}, 버튼표시={vis}")
    shot(pg, "02_align_tab")

    # ════ 1. 판 선택 사이클 (2회 반복) ════
    for cycle in (1, 2):
        tag = f"c{cycle}"
        # a) 판A 선택 시작 실클릭
        btn_ok = click_el(pg, "#dzw-curve-mode-btn")
        pg.wait_for_timeout(500)
        st = pg.evaluate(READ_MODE)
        report(f"{tag}-a 판A버튼→모드진입", btn_ok and st["mode"] == 1, f"클릭={btn_ok}, _faceMode={st['mode']}")

        pA, pB = pick_two_points(pg)
        if not pA:
            report(f"{tag}-b 캔버스 히트지점", False, "raycast 히트 없음"); continue

        # b) 판A 캔버스 실클릭 → 빨강
        r0, b0 = shot(pg, f"{tag}_0_before")
        pg.mouse.move(pA["x"], pA["y"]); pg.mouse.down(); pg.mouse.up()
        pg.wait_for_timeout(1500)
        r1, b1 = shot(pg, f"{tag}_1_afterA")
        st = pg.evaluate(READ_MODE)
        report(f"{tag}-b 판A클릭→빨강", r1 > r0 + 200 and st["a"] > 0,
               f"red {r0}→{r1}, regionA={st['a']}, mode={st['mode']}")

        # c) 판B 전환(우클릭, 실패 시 →판B 버튼) → 다른 판 클릭 → 파랑
        pg.mouse.click(pA["x"], pA["y"], button="right"); pg.wait_for_timeout(500)
        st = pg.evaluate(READ_MODE)
        via = "우클릭"
        if st["mode"] != 3:
            if click_el(pg, "#dzw-to-b-btn"): via = "→판B 버튼"
            pg.wait_for_timeout(400)
            st = pg.evaluate(READ_MODE)
        modeB = st["mode"] == 3
        if pB:
            pg.mouse.move(pB["x"], pB["y"]); pg.mouse.down(); pg.mouse.up()
            pg.wait_for_timeout(1500)
        r2, b2 = shot(pg, f"{tag}_2_afterB")
        st = pg.evaluate(READ_MODE)
        report(f"{tag}-c 판B전환({via})+클릭→파랑", modeB and b2 > b1 + 100 and st["b"] > 0,
               f"전환={modeB}, blue {b1}→{b2}, regionB={st['b']}")

        # d) 초기화 실클릭 → 색 제거
        click_el(pg, "#dzw-edge-reset-btn"); pg.wait_for_timeout(1000)
        r3, b3 = shot(pg, f"{tag}_3_reset")
        st = pg.evaluate(READ_MODE)
        report(f"{tag}-d 초기화→색제거", r3 < max(r1 * 0.3, 60) and st["a"] == 0 and st["b"] == 0,
               f"red {r1}→{r3}, blue {b2}→{b3}, mode={st['mode']}")

    # ════ 2. 스모크 ════
    base = pg.screenshot()
    for v in ("ISO", "TOP", "FIT"):
        ok = click_text(pg, v); pg.wait_for_timeout(1000)
        cur = pg.screenshot(); changed = cur != base; base = cur
        with open(os.path.join(OUT, f"view_{v}.png"), "wb") as f: f.write(cur)
        report(f"뷰 {v}", ok and changed, f"클릭={ok}, 픽셀변화={changed}")

    # 큰 화면 진입/복귀
    ok1 = click_text(pg, "큰 화면으로 보기", exact=False); pg.wait_for_timeout(2500)
    full = pg.evaluate("() => !!(window.__dzw.state && window.__dzw.state.fullRobot)")
    shot(pg, "smoke_full_in")
    ok2 = click_text(pg, "일반 화면", exact=False); pg.wait_for_timeout(2500)
    back = pg.evaluate("() => !(window.__dzw.state && window.__dzw.state.fullRobot)")
    shot(pg, "smoke_full_back")
    report("큰 화면 진입/복귀", ok1 and full and ok2 and back, f"진입버튼={ok1}/진입={full}, 복귀버튼={ok2}/복귀={back}")
    if not back:  # 복귀 실패 시 이후 테스트 위해 재시도
        click_text(pg, "일반 화면", exact=False); pg.wait_for_timeout(2000)

    # 포인트 추가 모드 → 캔버스 클릭 (티칭패널 ➕ 버튼 = dzw-mp-add-btn)
    ok = click_el(pg, "#dzw-mp-add-btn")
    src = "패널 ➕버튼"
    if not ok:
        ok = click_el(pg, '[data-dzw-pick="pointAdd"]'); src = "사이드바 포인트추가"
    pg.wait_for_timeout(600)
    mp = pg.evaluate("() => ({mode: !!window.__dzw._mpAddMode, tool: window.__dzw.state && window.__dzw.state.activeTool})")
    pA, _ = pick_two_points(pg)
    if pA: pg.mouse.click(pA["x"], pA["y"]); pg.wait_for_timeout(800)
    shot(pg, "smoke_pointAdd")
    report("포인트 추가 모드", ok and (mp["mode"] or mp["tool"] == "pointAdd"),
           f"{src} 클릭={ok}, _mpAddMode={mp['mode']}, tool={mp['tool']}")
    if mp["mode"]: click_el(pg, "#dzw-mp-add-btn"); pg.wait_for_timeout(300)  # 모드 해제

    # JOG 관절 조작 — 티칭패널 TCP 탭의 관절 슬라이더(dzw-js-S) 실드래그
    ok = click_el(pg, '.dzw-tp-tab[data-tab="tcp"]'); pg.wait_for_timeout(800)
    j0 = joints(pg)
    moved = False; how = "조작요소 없음"
    box = pg.locator("#dzw-js-S").bounding_box() if pg.locator("#dzw-js-S").count() else None
    if box and box["width"] > 0:
        pg.mouse.move(box["x"] + box["width"]*0.5, box["y"] + box["height"]/2)
        pg.mouse.down(); pg.mouse.move(box["x"] + box["width"]*0.6, box["y"] + box["height"]/2, steps=6); pg.mouse.up()
        how = "S축 슬라이더 드래그"
    pg.wait_for_timeout(1500)
    j1 = joints(pg)
    moved = j0 != j1
    shot(pg, "smoke_jog")
    report("JOG 관절 조작", ok and moved, f"패널={ok}, {how}, 관절변화={moved} {j0[:60]} → {j1[:60]}")

    # 재생 ▶ — JBI PROGRAM 트랜스포트 바의 삼각형 버튼 (svg polygon, 좌하단)
    pb = pg.evaluate("""() => {
      for (const p of document.querySelectorAll('svg polygon, svg path')) {
        const svg = p.closest('svg'); if (!svg) continue;
        const r = svg.getBoundingClientRect();
        if (r.left < 400 && r.top > 600 && r.width > 6 && r.width < 30) {
          const host = svg.parentElement, hr = host.getBoundingClientRect();
          if (hr.width > 20 && hr.width < 60) return {x: hr.left + hr.width/2, y: hr.top + hr.height/2};
        }
      }
      return null; }""")
    ok = False
    if pb:
        pg.mouse.click(pb["x"], pb["y"]); ok = True
    s0 = pg.screenshot(); pg.wait_for_timeout(1500)
    playing = pg.evaluate("() => !!(window.__dzw.state && (window.__dzw.state.playing || window.__dzw.state.mode==='PLAY'))")
    s1 = pg.screenshot()
    with open(os.path.join(OUT, "smoke_play.png"), "wb") as f: f.write(s1)
    report("재생 ▶", ok and (playing or s0 != s1), f"클릭={ok}, playing={playing}, 픽셀변화={s0!=s1}")
    if ok: click_el(pg, '[data-dzw-pick="play"]')  # 정지 시도

    # 상단 undo
    err_before = len([l for l in logs if "error" in l.lower() and "Failed to load resource" not in l])
    ok = click_el(pg, "#dzw-top-undo"); pg.wait_for_timeout(700)
    new_err = [l for l in logs if "error" in l.lower() and "Failed to load resource" not in l][err_before:]
    report("상단 undo", ok and not new_err, f"클릭={ok}, 신규에러={new_err[:2]}")

    # JBI 드롭다운
    ok = click_el(pg, '[data-dzw-pick="toggleJbiPicker"]')
    if not ok: ok = click_text(pg, "JBI 파일 선택", exact=False)
    pg.wait_for_timeout(1500)
    dd = pg.evaluate("""() => {
      const c = document.getElementById('dzw-jbi-dd') || document.querySelector('[id*="jbi"][id*="dd"]');
      if (c) { const r = c.getBoundingClientRect(); return {found: c.id, vis: r.width > 0 && r.height > 0}; }
      const S = window.__dzw.state || {}; return {found: 'state', vis: !!S.jbiPickerOpen || !!S.showJbiPicker};
    }""")
    shot(pg, "smoke_jbi_dd")
    report("JBI 드롭다운", bool(ok and dd and dd["vis"]), f"클릭={ok}, {dd}")

    with open(os.path.join(OUT, "console_log.txt"), "w", encoding="utf-8") as f:
        f.write("\n".join(logs))
    print("\n===== 결과표 =====")
    for n, s, e in results: print(f"{s:4} | {n} | {e}")
    fails = [r for r in results if r[1] == "FAIL"]
    print(f"\n총 {len(results)}개 중 실패 {len(fails)}개 / 스크린샷: {OUT}")
    br.close()
    sys.exit(1 if fails else 0)
