#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""P0 待办分层 —— 浏览器渲染实测 v2（工作台外壳正确进法，不碰生产）。

按技能 `baoyu-workbench-feature-increment/references/gated-page-browser-verification.md`：
- 外壳**不伪装飞书 UA**（否则被拉去 OAuth），改「灌会话 + 强制 state.user + 手动 showApp/渲染」
- 身份必须带 pwd/open_id，否则 init() 当场清掉并踢回登录页
- 从**工作台服务同源**（8765）进入；中文路径不预编码
- pushTodos 被桩掉并计数：证明「改类型 / 新增」确实会触发同步，而不真写 CloudBase
"""
import io
import json
import os
import sys
from playwright.sync_api import sync_playwright

W = '/Users/liuxinyuan/Desktop/Hermes输出-工作类'
BASE = 'http://127.0.0.1:8765/'
PAGE = '工具/index.html'
USER = '刘新元'
KEY = 'bior_sw_todos_v2_' + USER
REAL = json.load(io.open(os.path.join(W, '数据', 'todos.json'), encoding='utf-8'))['users'][USER]
seed = list(REAL)
seed.append({'id': 'legacyX', 'text': '旧格式兼容测试：cat=meeting', 'cat': 'meeting',
             'done': False, 'due': '2026-10-20', 'client': '', 'created': '2026-10-07'})
nOpen = len([t for t in seed if not t.get('done')])

ok = True
def chk(label, cond, detail=''):
    global ok
    print('  %s %-30s %s' % ('✅' if cond else '❌', label, detail))
    if not cond:
        ok = False

with sync_playwright() as p:
    br = p.chromium.launch(channel='chrome', headless=True)
    pg = br.new_page(viewport={'width': 1500, 'height': 1000})
    errs = []
    pg.on('pageerror', lambda e: errs.append(str(e)))
    pg.on('dialog', lambda d: d.accept())

    pg.goto(BASE, wait_until='domcontentloaded')
    pg.evaluate("""([u, k]) => {
        localStorage.setItem('bior_auth_token', 'preview-only-token');
        localStorage.setItem('bior_sw_user_v2', JSON.stringify({name:u, role:'admin', dept:'营销中心', line:'diag', open_id:'ou_preview_only'}));
        localStorage.removeItem(k);
    }""", [USER, KEY])

    pg.goto(BASE + PAGE, wait_until='load')
    pg.wait_for_timeout(2500)

    d0 = pg.evaluate("""() => ({
        title: document.title,
        href: location.href.slice(0, 90),
        bodyLen: document.body.innerText.length,
        switchTab: typeof window.switchTab,
        user: (window.state && window.state.user && window.state.user.name) || ''
    })""")
    print('外壳状态：title=%r switchTab=%s state.user=%r bodyLen=%d'
          % (d0['title'], d0['switchTab'], d0['user'], d0['bodyLen']))
    chk('外壳已进入（非登录页）', d0['switchTab'] == 'function' and d0['bodyLen'] > 800, d0['href'])

    pg.evaluate("""([u]) => {
        window.state = window.state || {};
        window.state.user = {name:u, role:'admin', dept:'营销中心', line:'diag', open_id:'ou_preview_only'};
        window.state.tab = 'todos';
        if (typeof showApp === 'function') showApp();
    }""", [USER])
    pg.wait_for_timeout(1200)

    pg.evaluate("""([k, seed]) => {
        window.__push = 0;
        pushTodos = function() { window.__push++; };
        window.state.tab = 'todos';
        localStorage.setItem(k, JSON.stringify(seed));
        render();
    }""", [KEY, seed])
    pg.wait_for_timeout(900)

    m = pg.evaluate("""(() => {
      const q = s => document.querySelectorAll(s);
      const T = e => (e ? e.innerText.replace(/\\s+/g,' ').trim() : '');
      const sel = document.getElementById('todoCat');
      const rows = [...q('.todo-item')];
      const heads = [...q('#main div, #main span')].filter(d => /^(📤 对外|🏠 对内)/.test(T(d)) && d.children.length === 0).map(T);
      return {
        rows: rows.length,
        heads: [...new Set(heads)],
        form: ['todoText','todoOwner','todoCat','todoClient','todoDue','todoTech'].filter(i => !!document.getElementById(i)),
        opts: sel ? [...sel.querySelectorAll('option')].map(o => o.value) : [],
        groups: sel ? [...sel.querySelectorAll('optgroup')].map(g => g.label) : [],
        legacy: rows.map(T).filter(t => t.indexOf('旧格式兼容测试') >= 0)[0] || '',
        inline: q('.todo-item select').length,
        owner: q('.todo-item .tag-blue').length,
        filters: [...q('.todo-filters button')].map(T),
        empty: !!document.querySelector('.todo-empty')
      };
    })()""")

    print('\n待办 Tab 实测（seed %d 条，未关闭 %d 条）：' % (len(seed), nOpen))
    chk('渲染行数=未关闭条数', m['rows'] == nOpen, '%d / 期望 %d' % (m['rows'], nOpen))
    chk('对外/对内分段标题', len(m['heads']) == 2, str(m['heads']))
    chk('表单字段齐全', len(m['form']) == 6, str(m['form']))
    chk('类型下拉 9 项', len(m['opts']) == 9, '%d 项 %s' % (len(m['opts']), m['opts']))
    chk('下拉分两组', len(m['groups']) == 2, str(m['groups']))
    chk('下拉不含旧 7 类', not (set(m['opts']) & {'prep','post','complaint','custom','meeting','travel','other'}))
    chk('旧格式兼容显示', ('邀约及计划' in m['legacy']) and ('旧格式兼容测试' in m['legacy']), m['legacy'][:60])
    chk('行内可改类型', m['inline'] == m['rows'], '%d 个' % m['inline'])
    chk('负责人标签', m['owner'] >= 1, '%d 个' % m['owner'])
    chk('范围筛选按钮', any('对外' in f for f in m['filters']) and any('对内' in f for f in m['filters']), str(m['filters']))

    tid = seed[0]['id']
    r = pg.evaluate("""(id) => {
      const b = window.__push;
      changeTodoType(id, 'internal_test_plan');
      const s = JSON.parse(localStorage.getItem('%s')||'[]').find(t=>t.id===id);
      return {pushed: window.__push-b, type:s&&s.type, cat:s&&s.cat, scope:s&&s.scope};
    }""" % KEY, tid)
    chk('改类型：三字段同步 + 触发同步', r['type'] == 'internal_test_plan' and r['cat'] == 'internal_test_plan'
        and r['scope'] == 'internal' and r['pushed'] == 1, json.dumps(r, ensure_ascii=False))

    b4 = pg.evaluate("() => loadTodos().length")
    pg.evaluate("""() => { document.getElementById('todoText').value='P0 测试条目';
                           document.getElementById('todoOwner').value='刘新元';
                           document.getElementById('todoDue').value=''; }""")
    pg.evaluate("addTodo()"); pg.wait_for_timeout(300)
    a1 = pg.evaluate("() => loadTodos().length")
    chk('新增：无截止被拦', a1 == b4, '%d → %d' % (b4, a1))
    pg.evaluate("() => { document.getElementById('todoDue').value='2026-11-30'; }")
    pg.evaluate("addTodo()"); pg.wait_for_timeout(400)
    top = pg.evaluate("() => { const l=loadTodos(); const t=l[0]; return {n:l.length, due:t.due, owner:t.owner, scope:t.scope, type:t.type, tech:t.tech_support}; }")
    chk('新增：落库带新字段', top['n'] == b4 + 1 and top['due'] == '2026-11-30' and top['owner'] == '刘新元'
        and top['type'] == 'external_visit' and top['scope'] == 'external', json.dumps(top, ensure_ascii=False))

    pg.evaluate("setTodoScope('external')"); pg.wait_for_timeout(500)
    h = pg.evaluate("() => [...new Set([...document.querySelectorAll('#main div, #main span')].filter(d=>/^(📤 对外|🏠 对内)/.test(d.innerText.replace(/\\s+/g,' ').trim())&&d.children.length===0).map(d=>d.innerText.replace(/\\s+/g,' ').trim()))]")
    chk('筛选=对外：只剩对外段', len(h) == 1 and '对外' in h[0], str(h))
    pg.evaluate("setTodoScope('all')"); pg.wait_for_timeout(400)

    chk('无 JS 错误', not errs, str(errs[:2]))
    pg.screenshot(path='/tmp/p0_todos.png', full_page=False)

    pg.evaluate("""([k]) => { localStorage.removeItem(k); localStorage.removeItem('bior_auth_token'); localStorage.removeItem('bior_sw_user_v2'); }""", [KEY])
    left = pg.evaluate("() => Object.keys(localStorage).filter(k => k.indexOf('bior_sw_todos') === 0).length")
    chk('预览数据已清', left == 0, '残留 %d' % left)
    br.close()

print('\n%s' % ('✅ P0 待办实测全部通过' if ok else '❌ 存在失败项'))
sys.exit(0 if ok else 1)
