#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""P0 待办分层 —— 浏览器渲染实测（预览会话，桩掉写库，不碰生产）。

要点：
- 用预览身份灌 localStorage（只为渲染，不调 API、不写生产）；
- `pushTodos` 被桩掉并计数，用来证明「改类型/新增」确实会触发同步，而不真的写 CloudBase；
- 额外塞一条**旧格式**（cat='meeting'）数据，验证兼容兜底能把它显示成「邀约及计划 / 对外」。
"""
import io
import json
import os
import re
import subprocess
import sys
import time
from playwright.sync_api import sync_playwright

W = '/Users/liuxinyuan/Desktop/Hermes输出-工作类'
PORT = 8812
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'})

srv = subprocess.Popen([sys.executable, '-m', 'http.server', str(PORT), '--directory', W],
                       stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
time.sleep(1.4)
url = 'http://127.0.0.1:%d/%s' % (PORT, PAGE)
ok = True


def chk(label, cond, detail=''):
    global ok
    print('  %s %s %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())

    # 1) 先到同源，灌预览会话 + 待办缓存
    pg.goto('http://127.0.0.1:%d/' % PORT, wait_until='domcontentloaded')
    pg.evaluate("""([u, k, seed]) => {
        localStorage.setItem('bior_auth_token', 'dev-preview-only');
        localStorage.setItem('bior_sw_user_v2', JSON.stringify({name: u, role: 'admin', line: 'diag'}));
        localStorage.setItem(k, JSON.stringify(seed));
    }""", [USER, KEY, seed])

    # 2) 打开待办页
    pg.goto(url, wait_until='load')
    pg.wait_for_timeout(1500)
    # 桩掉写库，并计数
    pg.evaluate("""() => { window.__push = 0; pushTodos = function() { window.__push++; }; }""")

    # 3) 切到待办 Tab
    pg.evaluate("switchTab('todos')")
    pg.wait_for_timeout(1600)

    m = pg.evaluate("""(() => {
      const q = s => document.querySelectorAll(s);
      const txt = e => (e ? e.innerText.replace(/\\s+/g, ' ').trim() : '');
      const sel = document.getElementById('todoCat');
      const opts = sel ? [...sel.querySelectorAll('option')].map(o => o.value) : [];
      const groups = sel ? [...sel.querySelectorAll('optgroup')].map(g => g.label) : [];
      // 找到段落标题（内含 对外 / 对内）
      const heads = [...q('#main div')].filter(d => /^(📤 对外|🏠 对内)/.test(txt(d)) && d.children.length === 0).map(txt);
      const rows = [...q('.todo-item')];
      return {
        rows: rows.length,
        heads: heads,
        form: ['todoOwner','todoCat','todoClient','todoDue','todoTech','todoText'].filter(i => !!document.getElementById(i)),
        opts, groups,
        selValCount: opts.length,
        // 旧格式那条渲染成什么
        legacy: rows.map(r => txt(r)).filter(t => t.indexOf('旧格式兼容测试') >= 0)[0] || '',
        // 空状态
        empty: !!document.querySelector('.todo-empty'),
        filters: [...q('.todo-filters button')].map(txt),
        hasInlineSel: q('.todo-item select').length,
        ownerTag: q('.todo-item .tag-blue').length
      };
    })()""")

    nOpen = len([t for t in seed if not t.get('done')])
    print('待办 Tab 实测：')
    chk('渲染行数 = 未关闭条数', m['rows'] == nOpen, '(%d / 期望 %d)' % (m['rows'], nOpen))
    chk('分段标题存在', len(m['heads']) >= 1, str(m['heads']))
    chk('表单字段齐全', len(m['form']) == 6, str(m['form']))
    chk('类型下拉 9 项', len(m['opts']) == 9, str(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'][:70])
    chk('行内可改类型', m['hasInlineSel'] == m['rows'], '%d 个' % m['hasInlineSel'])
    chk('负责人标签渲染', m['ownerTag'] >= 1, '%d 个' % m['ownerTag'])
    chk('范围筛选按钮齐', any('对外' in f for f in m['filters']) and any('对内' in f for f in m['filters']), str(m['filters']))

    # 4) 改类型 → 应写 localStorage 且触发 pushTodos
    tid = seed[0]['id']
    r = pg.evaluate("""(id) => {
      const before = window.__push;
      changeTodoType(id, 'external_appointment');
      const saved = JSON.parse(localStorage.getItem('%s') || '[]').find(t => t.id === id);
      return {pushed: window.__push - before, type: saved && saved.type,
              cat: saved && saved.cat, scope: saved && saved.scope};
    }""" % KEY, tid)
    chk('改类型：type/cat/scope 同步', r['type'] == 'external_appointment' and r['cat'] == 'external_appointment' and r['scope'] == 'external', json.dumps(r, ensure_ascii=False))
    chk('改类型：触发同步', r['pushed'] == 1, 'pushTodos 调用 %d 次' % r['pushed'])

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

    # 6) 范围筛选
    pg.evaluate("setTodoScope('external')")
    pg.wait_for_timeout(600)
    ext_heads = pg.evaluate("() => [...document.querySelectorAll('#main div')].filter(d => /^(📤 对外|🏠 对内)/.test(d.innerText.replace(/\\s+/g,' ').trim()) && d.children.length===0).map(d => d.innerText.replace(/\\s+/g,' ').trim())")
    chk('范围筛选=对外：只剩对外段', all('对外' in h for h in ext_heads) and len(ext_heads) == 1, str(ext_heads))
    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)

    # 7) 清理预览数据
    pg.evaluate("() => { localStorage.removeItem('%s'); 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()

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