/**
 * 通知 / 公告整合 smoke test —
 *
 * 覆盖:
 *   1. admin POST /admin/announcement/publish (空内容 → 400)
 *   2. admin POST /admin/announcement/publish (HTML 内容 → 200 sent_to=N)
 *   3. 后端 DB:fan-out 数等于 User 总数(active+inactive 都收到)
 *   4. tudou GET /announcement → 拿到那条 HTML
 *   5. tudou 浏览器进 /app 应自动强弹 modal(GlobalAnnouncementGate)
 *   6. 关闭 modal → POST /announcement/read → 再开页面不弹
 *   7. tudou 进消息中心 → 看到公告条目(带 [公告] chip + HTML 渲染)
 *
 * 前提:backend 8000 + frontend 5173 跑着,admin/admin123 + tudou/seaislee 可登录。
 * 清场:测试前清掉所有 announcement 未读;测试后也清掉以免污染其他 smoke。
 */
import { chromium } from '@playwright/test';

const BASE = 'http://localhost:5173';
const API = 'http://localhost:8000';

const results = [];
function pass(name) { results.push({ name, ok: true }); console.log(`  ✓ ${name}`); }
function fail(name, err) { results.push({ name, ok: false, err: err?.message || err }); console.log(`  ✗ ${name}: ${err?.message || err}`); }

async function login(page, username, password) {
  const res = await page.request.post(`${API}/api/v1/auth/login`, {
    data: { username, password },
  });
  if (!res.ok()) throw new Error(`登录失败 ${username}: ${res.status()}`);
  const body = await res.json();
  return { token: body?.tokens?.access, user: body?.user };
}

async function setStorage(page, { token, refresh, user }) {
  await page.goto(`${BASE}/login`, { waitUntil: 'domcontentloaded' });
  await page.evaluate(({ access, r, u }) => {
    localStorage.setItem('access_token', access);
    if (r) localStorage.setItem('refresh_token', r);
    if (u) localStorage.setItem('user', JSON.stringify(u));
  }, { access: token, r: refresh, u: user });
}

async function clearAllUnreadAnnouncements(adminToken) {
  // 没有专用清理 endpoint,用 admin 自己的 read-all 跑一遍(只清自己的);
  // 其他用户的未读靠 tudou 的 read-all 单独清。这里只清 admin 自己。
  await fetch(`${API}/api/v1/announcement/read`, {
    method: 'POST',
    headers: { 'Authorization': `Bearer ${adminToken}` },
  });
}

async function main() {
  const browser = await chromium.launch({ headless: true });
  const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
  const page = await ctx.newPage();
  const consoleErrors = [];
  page.on('console', (m) => {
    if (m.type() === 'error' && !/401|404|Failed to load|DevTools/.test(m.text())) {
      consoleErrors.push(m.text());
    }
  });
  // 接受 confirm 弹窗(发送公告二次确认)
  page.on('dialog', (d) => d.accept());

  console.log('\n════ 公告整合 smoke ════');

  // 前置:登录拿 token
  let adminTok, tudouTok;
  try {
    const a = await login(page, 'admin', 'admin123');
    const t = await login(page, 'tudou', 'seaislee');
    adminTok = a.token; tudouTok = t.token;
    if (!adminTok || !tudouTok) throw new Error('token 空');
  } catch (e) { fail('前置:admin/tudou 登录拿 token', e); await browser.close(); return; }

  // 先清掉 tudou 可能的旧未读公告
  await fetch(`${API}/api/v1/announcement/read`, {
    method: 'POST', headers: { 'Authorization': `Bearer ${tudouTok}` },
  });
  await fetch(`${API}/api/v1/announcement/read`, {
    method: 'POST', headers: { 'Authorization': `Bearer ${adminTok}` },
  });

  // ── 1. 空内容 400 ──
  try {
    const r = await fetch(`${API}/api/v1/admin/announcement/publish`, {
      method: 'POST',
      headers: { 'Authorization': `Bearer ${adminTok}`, 'Content-Type': 'application/json; charset=utf-8' },
      body: JSON.stringify({ content: '' }),
    });
    if (r.status === 400) pass('1. 空内容发送返回 400');
    else fail('1. 空内容', new Error(`期望 400 实际 ${r.status}`));
  } catch (e) { fail('1. 空内容', e); }

  // ── 2. HTML 发送返回 200 + sent_to ──
  // 内容包含多种颜色 — 既测 preview 截断 + accordion,也测颜色自适应:
  //   - 外层 div color:#e0e0e0  → 暗色专用灰白,应被 strip
  //   - h2 color:#a78bfa        → 紫色,应保留
  //   - span color:#34d399      → 绿色,应保留
  //   - hr border-top color:#374151 → 深灰,应被 strip
  // 用末尾独有 marker 字串作为"只在展开态才看得到"的探针(preview 60 字截断 '…')。
  const uniqueMarker = `EXPANDED-ONLY-MARKER-${Date.now()}`;
  const testContent = `<div style="color:#e0e0e0;line-height:1.8"><p>smoke 测试公告 1234567890abcdefghijklmnop 用一些很多文字填充让 preview 截断,确保折叠态看不到完整 HTML。</p><h2 style="color:#a78bfa">紫色标题应保留</h2><p>这里有个 <span style="color:#34d399">绿色字</span> 应保留,默认色 #e0e0e0 应被 strip。</p><hr style="border:none;border-top:1px solid #374151;margin:12px 0"><p>结尾标记 <b>${uniqueMarker}</b></p></div>`;
  let sentTo = 0;
  try {
    const r = await fetch(`${API}/api/v1/admin/announcement/publish`, {
      method: 'POST',
      headers: { 'Authorization': `Bearer ${adminTok}`, 'Content-Type': 'application/json; charset=utf-8' },
      body: JSON.stringify({ content: testContent }),
    });
    const data = await r.json();
    sentTo = data.sent_to;
    if (r.status === 200 && sentTo > 0) pass(`2. HTML 发送成功 sent_to=${sentTo}`);
    else fail('2. HTML 发送', new Error(`期望 200+sent_to>0 实际 ${r.status} ${JSON.stringify(data)}`));
  } catch (e) { fail('2. HTML 发送', e); }

  // ── 3. fan-out 数 = User 总数(用 admin 计:GET /admin/users total 应 ≈ sent_to) ──
  try {
    const r = await fetch(`${API}/api/v1/admin/users?page_size=1`, {
      headers: { 'Authorization': `Bearer ${adminTok}` },
    });
    const data = await r.json();
    if (data.total === sentTo) pass(`3. fan-out 数 (${sentTo}) = User 总数 (${data.total})`);
    else fail('3. fan-out 数', new Error(`sent_to=${sentTo} vs admin/users.total=${data.total}`));
  } catch (e) { fail('3. fan-out 数', e); }

  // ── 4. tudou GET /announcement 拿到那条 ──
  try {
    const r = await fetch(`${API}/api/v1/announcement`, {
      headers: { 'Authorization': `Bearer ${tudouTok}` },
    });
    const data = await r.json();
    if (data.enabled && data.announcement.includes('smoke 测试公告')) {
      pass('4. tudou 拿到未读公告');
    } else {
      fail('4. tudou 未读公告', new Error(`enabled=${data.enabled} content=${data.announcement.slice(0, 40)}`));
    }
  } catch (e) { fail('4. tudou GET /announcement', e); }

  // ── 5. tudou 浏览器进任意路由(/app)应自动弹 modal ──
  const tudouLogin = await login(page, 'tudou', 'seaislee');
  await setStorage(page, { token: tudouLogin.token, refresh: undefined, user: tudouLogin.user });
  await page.goto(`${BASE}/app`, { waitUntil: 'domcontentloaded' });
  await page.waitForTimeout(2000);  // 等 GlobalAnnouncementGate fetch + 渲染
  try {
    const modalVisible = await page.locator('text=我知道了').isVisible({ timeout: 3000 });
    if (modalVisible) pass('5. tudou 进 /app 自动强弹公告 modal');
    else fail('5. 自动强弹', new Error('未找到"我知道了"按钮 — modal 没弹'));
  } catch (e) { fail('5. 自动强弹', e); }

  // ── 6. 关闭 modal → 标已读 → 再开页面不弹 ──
  try {
    await page.locator('button:has-text("我知道了")').click({ timeout: 3000 });
    await page.waitForTimeout(1500);  // 等 POST /announcement/read 完成
    // 验证后端已标读
    const r = await fetch(`${API}/api/v1/announcement`, {
      headers: { 'Authorization': `Bearer ${tudouTok}` },
    });
    const data = await r.json();
    if (!data.enabled) pass('6. 关闭 modal 后 GET /announcement 返回 enabled=false (已读)');
    else fail('6. 关闭后未读取', new Error(`期望 enabled=false 实际 ${data.enabled}`));

    // 再开 /app 不该弹
    await page.goto(`${BASE}/admin/dashboard`, { waitUntil: 'domcontentloaded' });
    await page.waitForTimeout(2000);
    const stillVisible = await page.locator('button:has-text("我知道了")').isVisible({ timeout: 1500 }).catch(() => false);
    if (!stillVisible) pass('6.1 再开页面不再弹 modal');
    else fail('6.1 再次弹出', new Error('已读状态下还在弹'));
  } catch (e) { fail('6. 关闭流程', e); }

  // ── 7. 消息中心:accordion 列表 ──
  try {
    await page.goto(`${BASE}/notifications`, { waitUntil: 'domcontentloaded' });
    await page.waitForTimeout(2000);
    // 找带 [公告] chip 的行
    const chip = await page.locator('text=/^公告$/').first().isVisible({ timeout: 3000 });
    if (chip) pass('7. 消息中心显示 [公告] chip');
    else fail('7. chip 缺失', new Error('未找到公告 chip'));
    // 折叠态:公告文字应该在 title 行(标题"系统公告")就能看到
    const titleVisible = await page.locator('text=系统公告').first().isVisible({ timeout: 2000 });
    if (titleVisible) pass('7.1 折叠态显示公告标题');
    else fail('7.1 折叠态', new Error('看不到公告标题'));

    // ── 7.2 点击行展开 → 看到末尾独有 marker(只在展开态才能看到,折叠 preview 被 60 字 '…' 截断) ──
    // 用 chip [公告] 作为稳定点击锚点(chip 只在头部出现,不会跳到展开内容里)
    const chipLocator = page.locator('text=/^公告$/').first();
    // 先验证折叠态看不到 marker
    const markerInCollapsed = await page.locator(`text=${uniqueMarker}`).first().isVisible({ timeout: 1000 }).catch(() => false);
    if (!markerInCollapsed) pass('7.2.0 折叠态 preview 截断,看不到末尾 marker');
    else fail('7.2.0 折叠态泄漏', new Error('折叠状态下却看得到 marker,preview 没截断'));

    await chipLocator.click({ force: true, timeout: 3000 });
    await page.waitForTimeout(800);
    const expandedMarker = await page.locator(`text=${uniqueMarker}`).first().isVisible({ timeout: 2000 });
    if (expandedMarker) pass('7.2 点击行展开后显示完整 HTML 内容(看到末尾 marker)');
    else fail('7.2 展开内容', new Error('展开后看不到 marker'));

    // ── 7.3 再点 chip → 收起(marker 不再可见) ──
    await chipLocator.click({ force: true, timeout: 3000 });
    await page.waitForTimeout(800);
    const stillExpanded = await page.locator(`text=${uniqueMarker}`).first().isVisible({ timeout: 1000 }).catch(() => false);
    if (!stillExpanded) pass('7.3 再点同一行收起 (accordion,marker 不再可见)');
    else fail('7.3 收起', new Error('再点没收起,marker 还可见'));

    // ── 8. 颜色自适应:展开公告后检查渲染的 HTML inline color ──
    //    #e0e0e0 / #374151 (灰度暗色专用) → 应被 strip
    //    #a78bfa / #34d399 (彩色) → 应保留
    await chipLocator.click({ force: true, timeout: 3000 });  // 再次展开做颜色检查
    await page.waitForTimeout(800);
    const colors = await page.evaluate(() => {
      // 在 NotificationsPage 找展开区域的渲染 HTML(用 marker 锚定)
      const all = document.querySelectorAll('[style]');
      const result = { hasE0: false, hasA7: false, has34: false, has37: false };
      for (const el of all) {
        const s = el.getAttribute('style') || '';
        // 只看公告展开渲染区内的样式(排除非公告元素 — 例如带 marker 父链下)
        const inAnnouncement = el.closest('div') && (el.textContent || '').length < 300;
        if (s.includes('#e0e0e0') || s.includes('rgb(224')) result.hasE0 = true;
        if (s.includes('#a78bfa') || s.includes('rgb(167')) result.hasA7 = true;
        if (s.includes('#34d399') || s.includes('rgb(52')) result.has34 = true;
        if (s.includes('#374151') || s.includes('rgb(55')) result.has37 = true;
        // (inAnnouncement 用于将来如有需要可过滤,目前所有匹配都看)
      }
      return result;
    });
    if (!colors.hasE0) pass('8.1 #e0e0e0 (灰白) 已被 strip');
    else fail('8.1 #e0e0e0 未 strip', new Error('期望 strip 实际还在'));
    if (!colors.has37) pass('8.2 #374151 (深灰) 已被 strip');
    else fail('8.2 #374151 未 strip', new Error('期望 strip 实际还在'));
    if (colors.hasA7) pass('8.3 #a78bfa (紫色) 已保留');
    else fail('8.3 #a78bfa 被误 strip', new Error('彩色不该被 strip'));
    if (colors.has34) pass('8.4 #34d399 (绿色) 已保留');
    else fail('8.4 #34d399 被误 strip', new Error('彩色不该被 strip'));
  } catch (e) { fail('7. 消息中心 accordion', e); }

  // 清场:把测试造的公告标已读,避免污染下一次 smoke
  await fetch(`${API}/api/v1/announcement/read`, {
    method: 'POST', headers: { 'Authorization': `Bearer ${adminTok}` },
  });
  await fetch(`${API}/api/v1/announcement/read`, {
    method: 'POST', headers: { 'Authorization': `Bearer ${tudouTok}` },
  });

  if (consoleErrors.length > 0) {
    fail('console errors', new Error(consoleErrors.slice(0, 3).join(' | ')));
  } else {
    pass('全程无 console.error');
  }

  console.log('\n────────────── 汇总 ──────────────');
  const passed = results.filter(r => r.ok).length;
  const failed = results.filter(r => !r.ok);
  console.log(`通过: ${passed} / ${results.length}`);
  if (failed.length > 0) {
    console.log(`失败 ${failed.length} 项:`);
    failed.forEach(r => console.log(`  - ${r.name}: ${r.err}`));
  }

  await browser.close();
  process.exit(failed.length === 0 ? 0 : 1);
}

main().catch((e) => { console.error(e); process.exit(1); });
