#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""「客户拉通大表 · 表现形式对比 DEMO」渲染验收（playwright + 系统 Chrome，零外链自包含页）。

断言：
  1) 外部请求 = 0；无 console error
  2) 内联 SVG 守卫：window.__svgGuard() 为真；渲染后 SVG innerHTML 不含 title / br 标签
  3) 第一区三个甘特形态都能渲染（图形节点数 > 0）：形态1 圆点串 / 形态2 阶段色条 / 形态3 部门泳道
  4) 形态2 与形态3 用同一批数据（色条数一致），形态3 有泳道头
  5) 点击第一层某行 → 展开区事件节点数 > 3；再点另一行 → 内容切换
  6) 时间轴方向正确（卡片 left 递增 = 时间向右）
  7) 切形态 A→B 后事件仍可见（A: 事件卡 > 3；B: 泳道矩阵 chip > 0，且有「未触发」空行）
  8) 收合按钮可用
  9) 定稿基线 客户拉通大表_demo.html 与 工具/index.html、app.html mtime 未变
输出：5 张截图（3 个甘特形态 + 2 个展开形态）
"""
import os
import sys
import json

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PAGE = os.path.join(ROOT, '工具', '客户拉通大表_表现形式demo.html')
BASE_DEMO = os.path.join(ROOT, '工具', '客户拉通大表_demo.html')
INDEX = os.path.join(ROOT, '工具', 'index.html')
APP = os.path.join(ROOT, '工具', 'app.html')
SHOT = os.path.join(ROOT, '工具', '仪表盘', '_shots_thread_views')
os.makedirs(SHOT, exist_ok=True)

from playwright.sync_api import sync_playwright


def main():
    guard_paths = [BASE_DEMO, INDEX, APP]
    mtime_before = {p: os.path.getmtime(p) for p in guard_paths}
    res = {}
    ext, console = [], []

    with sync_playwright() as p:
        b = p.chromium.launch(channel='chrome')
        pg = b.new_page(viewport={'width': 2160, 'height': 1200})
        pg.on('request', lambda r: ext.append(r.url) if r.url.startswith(('http://', 'https://', '//')) else None)
        pg.on('console', lambda m: console.append({'type': m.type, 'text': m.text})
              if m.type in ('error', 'warning') else None)
        pg.goto('file://' + PAGE)
        pg.wait_for_timeout(700)

        res['demo_ok'] = pg.evaluate('()=>window.__demoOK||null')
        res['svg_guard_fn'] = pg.evaluate('()=>window.__svgGuard?window.__svgGuard():null')

        # ── 形态1 里程碑轨道 ──────────────────────────────
        res['form1_circles'] = pg.eval_on_selector_all('#ganttBox svg circle', 'els=>els.length')
        res['form1_rail'] = pg.eval_on_selector_all('#ganttBox svg line.rail', 'els=>els.length')
        res['form1_curlab'] = pg.eval_on_selector_all('#ganttBox svg .curlab', 'els=>els.length')
        res['form1_rows'] = pg.eval_on_selector_all('#ganttBox rect.rowhit', 'els=>els.length')
        el = pg.query_selector('#ganttBox'); el.scroll_into_view_if_needed()
        s1 = os.path.join(SHOT, 'form1_里程碑轨道.png'); el.screenshot(path=s1)
        res['shot_form1'] = s1

        # ── 形态2 阶段色条甘特 ────────────────────────────
        pg.click('.sw[data-f="2"]'); pg.wait_for_timeout(350)
        res['form2_bars'] = pg.eval_on_selector_all('#ganttBox rect.bar', 'els=>els.length')
        res['form2_today'] = pg.eval_on_selector_all('#ganttBox line.today', 'els=>els.length')
        res['form2_monthlbl'] = pg.eval_on_selector_all('#ganttBox text.mlbl', 'els=>els.length')
        res['form2_warnid'] = pg.evaluate(
            "()=>Array.from(document.querySelectorAll('#ganttBox text')).filter(t=>t.textContent.indexOf('待关联')>=0).length")
        res['form2_stuckbars'] = pg.evaluate(
            "()=>Array.from(document.querySelectorAll('#ganttBox rect.bar')).filter(r=>r.getAttribute('fill')==='#C0392B').map(r=>Math.round(+r.getAttribute('width')))")
        el = pg.query_selector('#ganttBox'); el.scroll_into_view_if_needed()
        s2 = os.path.join(SHOT, 'form2_阶段色条甘特.png'); el.screenshot(path=s2)
        res['shot_form2'] = s2

        # ── 形态3 部门泳道甘特 ────────────────────────────
        pg.click('.sw[data-f="3"]'); pg.wait_for_timeout(350)
        res['form3_bars'] = pg.eval_on_selector_all('#ganttBox rect.bar', 'els=>els.length')
        res['form3_lanehd'] = pg.evaluate(
            "()=>Array.from(document.querySelectorAll('#ganttBox text')).filter(t=>t.textContent.indexOf('泳道：')>=0).map(t=>t.textContent)")
        el = pg.query_selector('#ganttBox'); el.scroll_into_view_if_needed()
        s3 = os.path.join(SHOT, 'form3_部门泳道甘特.png'); el.screenshot(path=s3)
        res['shot_form3'] = s3

        # ── 点击第一层某行（大客户部 · 圣湘）→ 展开 ───────
        pg.evaluate("""()=>{
            const rs=document.querySelectorAll('#ganttBox rect.rowhit');
            rs[0].dispatchEvent(new MouseEvent('click',{bubbles:true}));
        }""")
        pg.wait_for_timeout(400)
        res['expand_after_click'] = pg.evaluate('()=>window.__expandInfo||null')
        res['expand_title'] = pg.eval_on_selector('.ex-title', 'e=>e.textContent')
        res['row_highlighted'] = pg.eval_on_selector_all('#ganttBox rect.rowhit.sel', 'els=>els.length')
        # 时间轴方向：卡片 left 递增
        res['ev_lefts'] = pg.eval_on_selector_all('.ev', 'els=>els.map(e=>Math.round(e.getBoundingClientRect().left))')
        res['ev_count_A'] = pg.eval_on_selector_all('.ev', 'els=>els.length')
        res['ev_dates'] = pg.eval_on_selector_all('.ev .ev-date', 'els=>els.map(e=>e.textContent)')
        # 点击一张卡（测试结果跟进，含 6 维 + 反馈流水）→ 下方详情面板展开完整内容
        pg.eval_on_selector('.ev[data-e="6"]', 'e=>e.click()'); pg.wait_for_timeout(250)
        res['ev_sel'] = pg.eval_on_selector_all('.ev.sel', 'els=>els.length')
        res['ev_detail_open'] = pg.eval_on_selector_all('#tlDetail .tl-dbody', 'els=>els.length')
        res['ev_detail_text'] = pg.eval_on_selector('#tlDetail', 'e=>e.textContent')
        res['ev_detail_has_six'] = '客户需求' in res['ev_detail_text']
        res['ev_detail_has_fb'] = '反馈流水' in res['ev_detail_text']
        el = pg.query_selector('#sec2'); el.scroll_into_view_if_needed(); pg.wait_for_timeout(200)
        sA = os.path.join(SHOT, 'expandA_横向时间轴.png'); el.screenshot(path=sA)
        res['shot_expandA'] = sA
        res['expand_h'] = pg.eval_on_selector('#sec2', 'e=>Math.round(e.getBoundingClientRect().height)')
        # 再点另一张卡（待办 / 订单）→ 详情面板切换内容
        pg.eval_on_selector('.ev[data-e="2"]', 'e=>e.click()'); pg.wait_for_timeout(250)
        res['ev_detail_text2'] = pg.eval_on_selector('#tlDetail', 'e=>e.textContent')
        res['ev_detail_has_todo'] = '待办事项' in res['ev_detail_text2']
        pg.eval_on_selector('.ev[data-e="1"]', 'e=>e.click()'); pg.wait_for_timeout(250)
        res['ev_detail_has_ord'] = '关联订单' in pg.eval_on_selector('#tlDetail', 'e=>e.textContent')

        # ── 切换形态 B 资源泳道矩阵 ───────────────────────
        pg.click('.sw[data-v="B"]'); pg.wait_for_timeout(400)
        res['mx_chips'] = pg.eval_on_selector_all('.mx-chip', 'els=>els.length')
        res['mx_lanehd'] = pg.eval_on_selector_all('.mx-lane', 'els=>els.map(e=>e.textContent)')
        res['mx_empty'] = pg.eval_on_selector_all('.mx-empty', 'els=>els.map(e=>e.textContent)')
        res['mx_after_B'] = pg.evaluate('()=>window.__expandInfo||null')
        el = pg.query_selector('#sec2'); el.scroll_into_view_if_needed(); pg.wait_for_timeout(200)
        sB = os.path.join(SHOT, 'expandB_资源泳道矩阵.png'); el.screenshot(path=sB)
        res['shot_expandB'] = sB

        # ── 再点另一行 → 内容切换 ─────────────────────────
        pg.evaluate("""()=>{
            const rs=document.querySelectorAll('#ganttBox rect.rowhit');
            rs[3].dispatchEvent(new MouseEvent('click',{bubbles:true}));
        }""")
        pg.wait_for_timeout(400)
        res['expand_title2'] = pg.eval_on_selector('.ex-title', 'e=>e.textContent')
        res['mx_chips2'] = pg.eval_on_selector_all('.mx-chip', 'els=>els.length')

        # ── 收合 ──────────────────────────────────────────
        pg.click('#btnCollapse'); pg.wait_for_timeout(300)
        res['collapsed'] = pg.evaluate('()=>window.__expandInfo && window.__expandInfo.sel===null')
        res['collapsed_ph'] = pg.eval_on_selector_all('.ph', 'els=>els.length')

        # ── 渲染后 SVG 质检（禁 title/br） ────────────────
        pg.click('.sw[data-f="1"]'); pg.wait_for_timeout(300)
        res['svg_guard_after'] = pg.evaluate('()=>window.__svgGuard?window.__svgGuard():null')
        res['svg_bad_inner'] = pg.eval_on_selector_all(
            'svg', "els=>els.map(e=>e.innerHTML).filter(h=>/<title[\\s>]|<br[\\s>]/i.test(h)).length")
        res['svg_count'] = pg.eval_on_selector_all('svg', 'els=>els.length')

        b.close()

    res['external_requests'] = ext
    res['console_errors'] = [c for c in console if c['type'] == 'error']
    res['console_all'] = console
    mtime_after = {p: os.path.getmtime(p) for p in guard_paths}
    res['mtime_unchanged'] = {os.path.basename(p): (mtime_before[p] == mtime_after[p]) for p in guard_paths}

    print(json.dumps({k: v for k, v in res.items()
                      if k not in ('console_all',)}, ensure_ascii=False, indent=2))

    lefts = res['ev_lefts']
    ok = []
    ok.append(('外部请求 = 0', len(ext) == 0))
    ok.append(('无 console error', len(res['console_errors']) == 0))
    ok.append(('SVG 守卫（构建期）通过', res['svg_guard_fn'] is True and res['svg_guard_after'] is True))
    ok.append(('渲染后 SVG 内无 title/br', res['svg_bad_inner'] == 0))
    ok.append(('第一区形态1 圆点串渲染（circle>40）', res['form1_circles'] > 40))
    ok.append(('形态1 有轨道连线与「当前节点」标签', res['form1_rail'] >= 4 and res['form1_curlab'] == 8))
    ok.append(('形态1 可点行 = 8', res['form1_rows'] == 8))
    ok.append(('第一区形态2 阶段色条渲染（rect.bar>0）', res['form2_bars'] > 0))
    ok.append(('形态2 有月份刻度 + 今天线', res['form2_monthlbl'] >= 8 and res['form2_today'] == 1))
    ok.append(('形态2 卡住/未回填 ID 用警示标注', res['form2_warnid'] >= 1))
    ok.append(('形态2 卡住节点为「未收口长条」（无 4 天残条）',
               len(res['form2_stuckbars']) >= 2 and all(w > 40 for w in res['form2_stuckbars'])))
    ok.append(('第一区形态3 部门泳道渲染（rect.bar>0）', res['form3_bars'] > 0))
    ok.append(('形态3 泳道头 4 个部门', len(res['form3_lanehd']) == 4))
    ok.append(('形态2 / 形态3 同一批数据（色条数一致）', res['form2_bars'] == res['form3_bars']))
    ok.append(('点击第一层某行 → 该行高亮', res['row_highlighted'] == 1))
    ok.append(('展开区带商机标题', '圣湘生物' in res['expand_title']))
    ok.append(('展开区事件节点数 > 3', res['ev_count_A'] > 3))
    ok.append(('时间轴方向向右（卡片 left 递增）', all(lefts[i] < lefts[i+1] for i in range(len(lefts)-1))))
    ok.append(('时间轴日期升序（首 < 末）', res['ev_dates'][0] < res['ev_dates'][-1]))
    ok.append(('点击事件卡 → 详情面板展开完整内容（6 维 / 反馈流水）',
               res['ev_sel'] == 1 and res['ev_detail_open'] >= 1
               and res['ev_detail_has_six'] is True and res['ev_detail_has_fb'] is True))
    ok.append(('点另一张卡 → 详情面板切换（待办 / 订单）',
               res['ev_detail_has_todo'] is True and res['ev_detail_has_ord'] is True))
    ok.append(('展开区高度不虚空（无大片空白）', 300 < res['expand_h'] < 900))
    ok.append(('切形态 B → 资源泳道矩阵 chip > 0', res['mx_chips'] > 0))
    ok.append(('形态B 5 条资源泳道就位', len(res['mx_lanehd']) == 5))
    ok.append(('形态B 空泳道显示「未触发」', any('未触发' in t for t in res['mx_empty'])))
    ok.append(('切形态后事件仍可见（A→B）', (res['mx_after_B'] or {}).get('view') == 'B'))
    ok.append(('再点另一行 → 内容切换', res['expand_title2'] != res['expand_title']))
    ok.append(('收合可用', res['collapsed'] is True and res['collapsed_ph'] >= 1))
    ok.append(('基线/首页/应用页 mtime 未变', all(res['mtime_unchanged'].values())))

    print('\n== 断言 ==')
    for n, v in ok:
        print(('  ✅ ' if v else '  ❌ ') + n)
    n_shot = [res['shot_form1'], res['shot_form2'], res['shot_form3'], res['shot_expandA'], res['shot_expandB']]
    print('\n截图（%d 张）:' % len(n_shot))
    for s in n_shot:
        print('  -', s, '%.1f KB' % (os.path.getsize(s)/1024.0))
    sys.exit(0 if all(v for _, v in ok) else 1)


if __name__ == '__main__':
    main()
