/**
 * 15分钟自动化采集测试（含6个异常阶段+数据对比验证）
 *
 * 测试流程：
 *   阶段0: 初始化 — 清空旧数据、启动模拟器、启动采集服务
 *   阶段1: 正常采集（5分钟）— 建立基准数据
 *   阶段2: HTTP 500异常（2分钟）— 模拟服务端错误
 *   阶段3: 超时异常（2分钟）— 模拟响应超时
 *   阶段4: 空数据返回（2分钟）— 模拟空响应
 *   阶段5: 拒绝连接（2分钟）— 模拟网络断开
 *   阶段6: 恢复正常采集（2分钟）— 验证恢复能力
 *   验证: 数据对比 — 模拟器汇总 vs 数据库记录
 *   报告: 输出详细测试报告到 docs/test-reports/
 *
 * 前置条件:
 *   1. MariaDB 运行中（cnc_business + cnc_log 库已初始化）
 *   2. CncSimulator 运行在网关端口 9000
 *   3. CncCollector 运行中，管理API在 http://localhost:5800/
 *
 * 安装: npm install @playwright/test mysql2
 * 运行: npx playwright test collector-15min.spec.ts --reporter=list --timeout=0
 *
 * 架构说明:
 *   网关(9000): /admin/api/start-address, /admin/api/stop-address, /admin/api/status(汇总)
 *   模拟端口(N): /admin/api/network, /admin/api/stats, /admin/api/full-summary 等
 */

import { test, expect, request } from '@playwright/test';
import mysql from 'mysql2/promise';
import * as fs from 'fs';
import * as path from 'path';

// ============================================================
// 常量与配置
// ============================================================

const API_BASE = 'http://localhost:5800';
const SIM_GATEWAY = 'http://localhost:9000';
const API_KEY = 'collector_api_key_2026';
const HEADERS = { 'X-API-Key': API_KEY, 'Content-Type': 'application/json' };

const DB_BUSINESS = {
  host: 'localhost', user: 'root', password: 'root',
  database: 'cnc_business', charset: 'utf8mb4'
};
const DB_LOG = {
  host: 'localhost', user: 'root', password: 'root',
  database: 'cnc_log', charset: 'utf8mb4'
};

/** 每个阶段的持续时间（毫秒） */
const PHASE_DURATIONS = {
  normal1: 5 * 60 * 1000,      // 阶段1: 正常采集5分钟
  http500: 2 * 60 * 1000,      // 阶段2: HTTP 500异常2分钟
  timeout: 2 * 60 * 1000,      // 阶段3: 超时异常2分钟
  empty: 2 * 60 * 1000,        // 阶段4: 空数据2分钟
  refuse: 2 * 60 * 1000,       // 阶段5: 拒绝连接2分钟
  normal2: 2 * 60 * 1000,      // 阶段6: 恢复正常2分钟
};

/** 测试报告输出路径 */
const REPORT_DIR = path.resolve(__dirname, '../../../docs/test-reports');

// ============================================================
// 数据库辅助函数
// ============================================================

async function queryBusiness(sql: string, params?: any[]) {
  const conn = await mysql.createConnection(DB_BUSINESS);
  try {
    const [rows] = await conn.query(sql, params);
    return rows as any[];
  } finally {
    await conn.end();
  }
}

async function queryLog(sql: string, params?: any[]) {
  const conn = await mysql.createConnection(DB_LOG);
  try {
    const [rows] = await conn.query(sql, params);
    return rows as any[];
  } finally {
    await conn.end();
  }
}

async function executeBusiness(sql: string, params?: any[]) {
  const conn = await mysql.createConnection(DB_BUSINESS);
  try {
    await conn.execute(sql, params);
  } finally {
    await conn.end();
  }
}

async function executeLog(sql: string, params?: any[]) {
  const conn = await mysql.createConnection(DB_LOG);
  try {
    await conn.execute(sql, params);
  } finally {
    await conn.end();
  }
}

/** 辅助：调用采集服务管理API */
async function collectorApi(method: string, path: string) {
  const ctx = await request.newContext();
  const opts: any = { headers: HEADERS };
  if (method === 'GET') return ctx.get(`${API_BASE}${path}`, opts);
  return ctx.post(`${API_BASE}${path}`, opts);
}

/** 辅助：等待指定毫秒 */
const sleep = (ms: number) => new Promise(r => setTimeout(r, ms));

/** 辅助：调用模拟器**单地址**管理API（stats/network/full-summary等） */
async function simApi(path: string, body?: any) {
  const ctx = await request.newContext({ timeout: 10000 });
  if (body) {
    return ctx.post(`http://localhost:${simPort}${path}`, { data: body, headers: { 'Content-Type': 'application/json' } });
  }
  return ctx.get(`http://localhost:${simPort}${path}`);
}

/** 辅助：调用模拟器**网关**API（start-address/stop-address/status等） */
async function gatewayApi(path: string, body?: any) {
  const ctx = await request.newContext({ timeout: 10000 });
  if (body) {
    return ctx.post(`${SIM_GATEWAY}${path}`, { data: body, headers: { 'Content-Type': 'application/json' } });
  }
  return ctx.get(`${SIM_GATEWAY}${path}`);
}

/** 日志输出带时间戳 */
function log(msg: string) {
  console.log(`[${new Date().toLocaleTimeString('zh-CN')}] ${msg}`);
}

// ============================================================
// 全局变量
// ============================================================

/** 模拟地址端口（由start-address动态分配） */
let simPort = 0;
let testStartTime: Date;
let phaseTimestamps: Record<string, Date> = {};
let phaseSnapshots: Record<string, any> = {};
let globalMismatchCount = 0;

// ============================================================
// 全局串行配置
// ============================================================

test.describe.configure({ mode: 'serial' });

// ============================================================
// 测试主流程
// ============================================================

test.describe('15分钟自动化采集测试', () => {

  test('阶段0: 初始化 — 清空数据并启动服务', async () => {
    test.setTimeout(120000);
    testStartTime = new Date();
    log('\n===== 15分钟自动化采集测试开始 =====');
    log(`测试开始时间: ${testStartTime.toLocaleString('zh-CN')}`);

    // 1. 清空采集数据
    log('[0/7] 清空旧采集数据...');
    await executeBusiness('TRUNCATE TABLE cnc_collect_record');
    await executeBusiness('TRUNCATE TABLE cnc_production_segment');
    await executeLog('TRUNCATE TABLE log_collect_raw');
    log('  ✓ 已清空 cnc_collect_record, cnc_production_segment, log_collect_raw');

    // 2. 确认采集地址配置
    log('[1/7] 确认采集地址配置...');
    const addrs = await queryBusiness('SELECT id, url, is_enabled, collect_interval FROM cnc_collect_address WHERE id = 1');
    expect(addrs.length).toBe(1);
    log(`  ✓ 采集地址: id=${addrs[0].id}, interval=${addrs[0].collect_interval}s`);

    // 3. 确认机床数据
    log('[2/7] 确认机床数据...');
    const machines = await queryBusiness(
      'SELECT id, device_code, name, collect_address_id, is_enabled FROM cnc_machine WHERE collect_address_id = 1 AND is_enabled = 1'
    );
    expect(machines.length).toBeGreaterThanOrEqual(10);
    log(`  ✓ 机床数量: ${machines.length}台`);

    // 4. 通过网关启动地址模拟
    log('[3/7] 通过网关启动模拟...');
    const startResp = await gatewayApi('/admin/api/start-address', {
      dbAddressId: 1,
      interval: 10,
      deviceCodes: machines.map((m: any) => m.device_code)
    });
    expect(startResp.ok()).toBeTruthy();
    const startResult = await startResp.json() as any;
    simPort = startResult.port;
    expect(simPort).toBeGreaterThan(0);
    log(`  ✓ 模拟地址已启动 → 端口 ${simPort}`);

    // 5. 等待模拟器数据就绪，确认单地址API可用
    await sleep(3000);
    const dataResp = await simApi('/admin/api/status');
    if (dataResp.ok()) {
      const status = await dataResp.json() as any;
      log(`  ✓ 模拟地址状态: ${status.totalDevices}台设备, 在线${status.onlineDevices}台, 运行=${status.isRunning}`);
    }

    // 6. 更新采集地址URL指向模拟端口
    await executeBusiness(
      'UPDATE cnc_collect_address SET url = ?, is_enabled = 1 WHERE id = 1',
      [`http://localhost:${simPort}/`]
    );
    log(`  ✓ 更新采集地址URL → http://localhost:${simPort}/`);

    // 7. 启动采集服务
    log('[4/7] 启动采集服务...');
    await collectorApi('POST', '/api/collector/stop');
    await sleep(1000);
    await collectorApi('POST', '/api/collector/refresh');
    await sleep(500);
    await collectorApi('POST', '/api/collector/start');
    log('  ✓ 采集服务已启动');

    // 8. 等待2个采集周期确认数据流
    log('[5/7] 等待数据采集确认...');
    await sleep(25000);

    const records = await queryBusiness(
      'SELECT COUNT(*) as cnt FROM cnc_collect_record WHERE collect_time > ?',
      [testStartTime]
    );
    const rawLogs = await queryLog(
      'SELECT COUNT(*) as cnt FROM log_collect_raw WHERE request_time > ?',
      [testStartTime]
    );
    log(`  ✓ 采集记录数: ${records[0].cnt}, 原始日志数: ${rawLogs[0].cnt}`);
    expect(records[0].cnt).toBeGreaterThan(0);

    // 8.5 验证后台仪表盘API有数据（确认IIS+前端能拿到采集数据）
    log('[5.5/7] 验证后台仪表盘API...');
    try {
      const loginResp = await fetch('http://127.0.0.1/api/admin/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ username: 'admin', password: 'admin123' })
      });
      if (loginResp.ok) {
        const loginData = await loginResp.json() as any;
        const token = loginData.data?.token;
        if (token) {
          const dashResp = await fetch('http://127.0.0.1/api/admin/dashboard/summary', {
            headers: { Authorization: `Bearer ${token}` }
          });
          if (dashResp.ok) {
            const dashData = await dashResp.json() as any;
            const d = dashData.data;
            log(`  ✓ 后台仪表盘: 在线机床=${d.onlineCount}/${d.totalMachines}, 成功率=${d.collectSuccessRate?.toFixed(1)}%, 告警=${d.activeAlerts}`);
          }
        }
      }
    } catch {
      log('  ⚠ 后台API不可达（IIS可能未配置），跳过验证');
    }

    // 9. 保存初始快照
    phaseTimestamps['init'] = new Date();
    const snapshot0 = await simApi('/admin/api/full-summary');
    if (snapshot0.ok()) phaseSnapshots['init'] = await snapshot0.json();

    log('[6/7] 初始化完成');
    log(`[7/7] 开始15分钟采集测试（网关=${SIM_GATEWAY}, 模拟端口=${simPort}）\n`);
  });

  // ============================================================
  // 阶段1: 正常采集（5分钟）
  // ============================================================

  test('阶段1: 正常采集 — 5分钟建立基准数据', async () => {
    test.setTimeout(PHASE_DURATIONS.normal1 + 60000);
    log('━━━ 阶段1: 正常采集（5分钟） ━━━');
    phaseTimestamps['normal1_start'] = new Date();

    const statsBefore = await simApi('/admin/api/stats');
    const statsBeforeData = statsBefore.ok() ? await statsBefore.json() : {};

    log(`  开始: 总零件=${statsBeforeData.totalParts || '?'}, 在线设备=${statsBeforeData.onlineDevices || '?'}`);
    log(`  等待 ${PHASE_DURATIONS.normal1 / 1000} 秒...`);

    await sleep(PHASE_DURATIONS.normal1);

    phaseTimestamps['normal1_end'] = new Date();
    const statsAfter = await simApi('/admin/api/stats');
    const statsAfterData = statsAfter.ok() ? await statsAfter.json() : {};

    const snapshot = await simApi('/admin/api/full-summary');
    if (snapshot.ok()) phaseSnapshots['normal1'] = await snapshot.json();

    log(`  结束: 总零件=${statsAfterData.totalParts || '?'}, 在线设备=${statsAfterData.onlineDevices || '?'}`);
    log(`  ✓ 阶段1完成\n`);
  });

  // ============================================================
  // 阶段2: HTTP 500异常（2分钟）
  // ============================================================

  test('阶段2: HTTP 500异常 — 2分钟', async () => {
    test.setTimeout(PHASE_DURATIONS.http500 + 60000);
    log('━━━ 阶段2: HTTP 500异常（2分钟） ━━━');
    phaseTimestamps['http500_start'] = new Date();

    const setResp = await simApi('/admin/api/network', { type: 'http500' });
    log(`  ✓ 已设置网络异常: HTTP 500 (ok=${setResp.ok()})`);

    log(`  等待 ${PHASE_DURATIONS.http500 / 1000} 秒...`);
    await sleep(PHASE_DURATIONS.http500);

    phaseTimestamps['http500_end'] = new Date();

    const snapshot = await simApi('/admin/api/full-summary');
    if (snapshot.ok()) phaseSnapshots['http500'] = await snapshot.json();

    await simApi('/admin/api/network', { type: 'normal' });
    log('  ✓ 已恢复正常网络');
    log(`  ✓ 阶段2完成\n`);
  });

  // ============================================================
  // 阶段3: 超时异常（2分钟）
  // ============================================================

  test('阶段3: 超时异常 — 2分钟', async () => {
    test.setTimeout(PHASE_DURATIONS.timeout + 60000);
    log('━━━ 阶段3: 超时异常（2分钟） ━━━');
    phaseTimestamps['timeout_start'] = new Date();

    const setResp = await simApi('/admin/api/network', { type: 'timeout' });
    log(`  ✓ 已设置网络异常: 超时(60秒延迟) (ok=${setResp.ok()})`);

    log(`  等待 ${PHASE_DURATIONS.timeout / 1000} 秒...`);
    await sleep(PHASE_DURATIONS.timeout);

    phaseTimestamps['timeout_end'] = new Date();

    const snapshot = await simApi('/admin/api/full-summary');
    if (snapshot.ok()) phaseSnapshots['timeout'] = await snapshot.json();

    await simApi('/admin/api/network', { type: 'normal' });
    await sleep(2000);
    log('  ✓ 已恢复正常网络');
    log(`  ✓ 阶段3完成\n`);
  });

  // ============================================================
  // 阶段4: 空数据返回（2分钟）
  // ============================================================

  test('阶段4: 空数据返回 — 2分钟', async () => {
    test.setTimeout(PHASE_DURATIONS.empty + 60000);
    log('━━━ 阶段4: 空数据返回（2分钟） ━━━');
    phaseTimestamps['empty_start'] = new Date();

    const setResp = await simApi('/admin/api/network', { type: 'empty' });
    log(`  ✓ 已设置网络异常: 空数据([]) (ok=${setResp.ok()})`);

    log(`  等待 ${PHASE_DURATIONS.empty / 1000} 秒...`);
    await sleep(PHASE_DURATIONS.empty);

    phaseTimestamps['empty_end'] = new Date();

    const snapshot = await simApi('/admin/api/full-summary');
    if (snapshot.ok()) phaseSnapshots['empty'] = await snapshot.json();

    await simApi('/admin/api/network', { type: 'normal' });
    await sleep(2000);
    log('  ✓ 已恢复正常网络');
    log(`  ✓ 阶段4完成\n`);
  });

  // ============================================================
  // 阶段5: 拒绝连接（2分钟）
  // ============================================================

  test('阶段5: 拒绝连接 — 2分钟', async () => {
    test.setTimeout(PHASE_DURATIONS.refuse + 60000);
    log('━━━ 阶段5: 拒绝连接（2分钟） ━━━');
    phaseTimestamps['refuse_start'] = new Date();

    // 拒绝连接会停止HttpListener，但管理API仍需通过同一端口操作
    // 这里用网关API直接发送（但网关没有network命令，必须发到模拟端口）
    // 拒绝连接意味着模拟端口的HttpListener被停止，无法再发请求
    // 解决方案：先记录快照，再设置拒绝
    const snapshotBefore = await simApi('/admin/api/full-summary');
    if (snapshotBefore.ok()) phaseSnapshots['refuse_before'] = await snapshotBefore.json();

    try {
      const setResp = await simApi('/admin/api/network', { type: 'refuse' });
      log(`  ✓ 已设置网络异常: 拒绝连接 (ok=${setResp.ok()})`);
    } catch {
      log('  ⚠ 设置拒绝连接时连接已断开（正常现象）');
    }

    log(`  等待 ${PHASE_DURATIONS.refuse / 1000} 秒...`);
    await sleep(PHASE_DURATIONS.refuse);

    phaseTimestamps['refuse_end'] = new Date();

    // 拒绝连接期间无法访问模拟端口
    log('  (拒绝连接期间无法获取模拟器快照，使用设置前的快照)');
    phaseSnapshots['refuse'] = phaseSnapshots['refuse_before'];

    // 恢复网络（此时HttpListener已停止，需要等它重启）
    try {
      await simApi('/admin/api/network', { type: 'normal' });
    } catch {
      log('  (恢复请求发送到模拟端口失败，因为HttpListener已停止)');
    }
    // 等待HttpListener恢复
    await sleep(3000);
    log('  ✓ 已恢复正常网络');
    log(`  ✓ 阶段5完成\n`);
  });

  // ============================================================
  // 阶段6: 恢复正常采集（2分钟）
  // ============================================================

  test('阶段6: 恢复正常采集 — 2分钟', async () => {
    test.setTimeout(PHASE_DURATIONS.normal2 + 120000);
    log('━━━ 阶段6: 恢复正常采集（2分钟） ━━━');

    // 拒绝连接后HttpListener已停止，通过网关重启模拟地址
    // 关键：指定使用原来的端口，避免URL不同步
    log('  通过网关重启模拟地址（保持原端口）...');
    await gatewayApi('/admin/api/stop-address', { dbAddressId: 1 });
    await sleep(2000);
    const restartResp = await gatewayApi('/admin/api/start-address', {
      dbAddressId: 1, interval: 10, port: simPort
    });
    if (restartResp.ok()) {
      const result = await restartResp.json() as any;
      const newPort = result.port || simPort;
      if (newPort !== simPort) {
        log(`  ⚠ 端口变化: ${simPort} → ${newPort}，需更新采集地址`);
        simPort = newPort;
        await executeBusiness(
          'UPDATE cnc_collect_address SET url = ? WHERE id = 1',
          [`http://localhost:${simPort}/`]
        );
        // 刷新采集服务配置以加载新URL
        await collectorApi('POST', '/api/collector/refresh');
        await sleep(2000);
      }
      log(`  ✓ 模拟地址已重启 → 端口 ${simPort}`);
    } else {
      log('  ⚠ 模拟地址重启失败');
    }
    await sleep(3000);

    // 确认模拟地址正常运行
    try {
      const dataCheck = await simApi('/admin/api/status');
      if (dataCheck.ok()) {
        const status = await dataCheck.json() as any;
        log(`  ✓ 模拟地址恢复: 在线${status.onlineDevices}台, 运行=${status.isRunning}`);
      }
    } catch (e) {
      log(`  ⚠ 模拟地址检查失败: ${e}，继续测试`);
    }

    phaseTimestamps['normal2_start'] = new Date();

    log(`  等待 ${PHASE_DURATIONS.normal2 / 1000} 秒...`);
    await sleep(PHASE_DURATIONS.normal2);

    phaseTimestamps['normal2_end'] = new Date();

    // 保存最终快照
    try {
      const snapshot = await simApi('/admin/api/full-summary');
      if (snapshot.ok()) phaseSnapshots['normal2'] = await snapshot.json();
    } catch { /* 忽略 */ }

    // 停止采集服务（触发service_stop结账）
    await collectorApi('POST', '/api/collector/stop');
    log('  ✓ 采集服务已停止（触发service_stop结账）');
    await sleep(3000);

    // 停止模拟器
    await gatewayApi('/admin/api/stop-address', { dbAddressId: 1 });
    log('  ✓ 模拟地址已停止');

    log(`  ✓ 阶段6完成\n`);
  });

  // ============================================================
  // 数据对比验证
  // ============================================================

  test('验证: 数据完整性对比', async () => {
    test.setTimeout(120000);
    log('━━━ 数据完整性对比验证 ━━━');

    // 1. 获取模拟器最终汇总
    let simSummary: any = phaseSnapshots['normal2'] || {};
    try {
      const resp = await simApi('/admin/api/full-summary');
      if (resp.ok()) simSummary = await resp.json();
    } catch { /* 模拟器可能已停止 */ }

    log('\n  [1/5] 模拟器最终汇总:');
    log(`    总设备: ${simSummary.totalDevices || '?'}`);
    log(`    总零件: ${simSummary.totalParts || '?'}`);
    log(`    异常数: ${simSummary.errorCount || 0}`);

    // 2. 查询数据库统计
    const dbRecords = await queryBusiness(
      'SELECT COUNT(*) as cnt FROM cnc_collect_record WHERE collect_time > ?',
      [testStartTime]
    );
    const dbSegments = await queryBusiness(
      'SELECT COUNT(*) as cnt FROM cnc_production_segment WHERE created_at > ?',
      [testStartTime]
    );
    const dbSettled = await queryBusiness(
      'SELECT COUNT(*) as cnt FROM cnc_production_segment WHERE is_settled = 1 AND created_at > ?',
      [testStartTime]
    );
    const dbActive = await queryBusiness(
      'SELECT COUNT(*) as cnt FROM cnc_production_segment WHERE is_settled = 0 AND created_at > ?',
      [testStartTime]
    );
    const dbRawLogs = await queryLog(
      'SELECT COUNT(*) as cnt FROM log_collect_raw WHERE request_time > ?',
      [testStartTime]
    );
    const dbSuccessLogs = await queryLog(
      'SELECT COUNT(*) as cnt FROM log_collect_raw WHERE is_success = 1 AND request_time > ?',
      [testStartTime]
    );
    const dbFailLogs = await queryLog(
      'SELECT COUNT(*) as cnt FROM log_collect_raw WHERE is_success = 0 AND request_time > ?',
      [testStartTime]
    );

    log('\n  [2/5] 数据库统计:');
    log(`    采集记录数: ${dbRecords[0].cnt}`);
    log(`    产量分段数: ${dbSegments[0].cnt} (已结账=${dbSettled[0].cnt}, 未结账=${dbActive[0].cnt})`);
    log(`    原始日志数: ${dbRawLogs[0].cnt} (成功=${dbSuccessLogs[0].cnt}, 失败=${dbFailLogs[0].cnt})`);

    expect(dbRecords[0].cnt).toBeGreaterThan(0);
    expect(dbRawLogs[0].cnt).toBeGreaterThan(0);
    log('  ✓ 采集记录和原始日志均已产生');

    // 3. 按设备对比零件数
    log('\n  [3/5] 按设备零件数对比:');
    const simParts = simSummary.devices || [];
    let matchCount = 0;
    let mismatchCount = 0;
    let mismatchDetails: string[] = [];

    for (const simDev of simParts) {
      const deviceCode = simDev.deviceCode;
      const simTotal = simDev.totalParts || 0;

      const machine = await queryBusiness(
        'SELECT id FROM cnc_machine WHERE device_code = ?',
        [deviceCode]
      );
      if (machine.length === 0) {
        log(`    ⚠ ${deviceCode}: 数据库中未找到对应机床`);
        continue;
      }

      const segments = await queryBusiness(
        'SELECT program_name, quantity, is_settled, close_reason FROM cnc_production_segment WHERE machine_id = ? AND created_at > ?',
        [machine[0].id, testStartTime]
      );

      let dbTotal = 0;
      for (const seg of segments) {
        dbTotal += Number(seg.quantity) || 0;
      }

      const diff = Math.abs(simTotal - dbTotal);
      const tolerance = Math.max(simTotal * 0.15, 5); // 允许15%或5个的误差（考虑异常期间）

      if (diff <= tolerance) {
        matchCount++;
        if (diff > 0) {
          log(`    ≈ ${deviceCode}: 模拟器=${simTotal}, 数据库=${dbTotal} (差${diff}, 在容差内)`);
        }
      } else {
        mismatchCount++;
        const detail = `    ✗ ${deviceCode}: 模拟器=${simTotal}, 数据库=${dbTotal} (差${diff})`;
        mismatchDetails.push(detail);
        log(detail);
      }
    }

    log(`\n    匹配: ${matchCount}台, 不匹配: ${mismatchCount}台`);
    if (mismatchCount === 0) {
      log('  ✓ 所有设备零件数完全匹配！');
    } else {
      log(`  ⚠ 有 ${mismatchCount} 台设备零件数不匹配，详见上方`);
    }
    globalMismatchCount = mismatchCount;

    // 4. 验证分段结账正确性
    log('\n  [4/5] 分段结账验证:');
    const closeReasons = await queryBusiness(
      `SELECT close_reason, COUNT(*) as cnt FROM cnc_production_segment
       WHERE created_at > ? GROUP BY close_reason`,
      [testStartTime]
    );
    for (const row of closeReasons) {
      log(`    ${row.close_reason || 'NULL'}: ${row.cnt}条`);
    }

    const unsettled = await queryBusiness(
      'SELECT machine_id, program_name FROM cnc_production_segment WHERE is_settled = 0 AND created_at > ?',
      [testStartTime]
    );
    if (unsettled.length === 0) {
      log('  ✓ 所有分段已结账');
    } else {
      log(`  ⚠ 有 ${unsettled.length} 条分段未结账`);
    }

    // 5. 验证异常期间的数据
    log('\n  [5/5] 异常期间数据验证:');

    const http500Logs = await queryLog(
      `SELECT COUNT(*) as cnt FROM log_collect_raw
       WHERE is_success = 0
       AND request_time BETWEEN ? AND ?`,
      [phaseTimestamps['http500_start'], phaseTimestamps['http500_end']]
    );
    log(`    HTTP 500期间失败日志: ${http500Logs[0]?.cnt || 0}条`);

    const refuseLogs = await queryLog(
      `SELECT COUNT(*) as cnt FROM log_collect_raw
       WHERE is_success = 0
       AND request_time BETWEEN ? AND ?`,
      [phaseTimestamps['refuse_start'], phaseTimestamps['refuse_end']]
    );
    log(`    拒绝连接期间失败日志: ${refuseLogs[0]?.cnt || 0}条`);

    const recoveryRecords = await queryBusiness(
      `SELECT COUNT(*) as cnt FROM cnc_collect_record
       WHERE collect_time > ?`,
      [phaseTimestamps['normal2_start']]
    );
    log(`    恢复后新增采集记录: ${recoveryRecords[0].cnt}条`);
    expect(recoveryRecords[0].cnt).toBeGreaterThan(0);
    log('  ✓ 恢复后数据正常产生');

    log('\n  ✓ 数据完整性对比完成\n');
  });

  // ============================================================
  // 生成测试报告
  // ============================================================

  test('报告: 生成15分钟采集测试报告', async () => {
    test.setTimeout(30000);

    if (!fs.existsSync(REPORT_DIR)) {
      fs.mkdirSync(REPORT_DIR, { recursive: true });
    }

    const timestamp = new Date().toISOString().replace(/[:.]/g, '').slice(0, 15);
    const reportPath = path.join(REPORT_DIR, `${timestamp}-15分钟采集测试报告.md`);

    // 收集最终数据
    let simSummary: any = phaseSnapshots['normal2'] || {};
    try {
      const resp = await simApi('/admin/api/full-summary');
      if (resp.ok()) simSummary = await resp.json();
    } catch { /* 模拟器可能已停止 */ }

    let eventHistoryData: any[] = [];
    try {
      const eventHistory = await simApi('/admin/api/event-history');
      if (eventHistory.ok()) eventHistoryData = await eventHistory.json();
    } catch {}

    let errorLogData: any[] = [];
    try {
      const errorLog = await simApi('/admin/api/error-log');
      if (errorLog.ok()) errorLogData = await errorLog.json();
    } catch {}

    const dbRecords = await queryBusiness(
      'SELECT COUNT(*) as cnt FROM cnc_collect_record WHERE collect_time > ?',
      [testStartTime]
    );
    const dbSegments = await queryBusiness(
      'SELECT COUNT(*) as cnt FROM cnc_production_segment WHERE created_at > ?',
      [testStartTime]
    );
    const dbSettled = await queryBusiness(
      'SELECT COUNT(*) as cnt FROM cnc_production_segment WHERE is_settled = 1 AND created_at > ?',
      [testStartTime]
    );
    const dbRawLogs = await queryLog(
      'SELECT COUNT(*) as cnt FROM log_collect_raw WHERE request_time > ?',
      [testStartTime]
    );
    const dbSuccessLogs = await queryLog(
      'SELECT COUNT(*) as cnt FROM log_collect_raw WHERE is_success = 1 AND request_time > ?',
      [testStartTime]
    );
    const dbFailLogs = await queryLog(
      'SELECT COUNT(*) as cnt FROM log_collect_raw WHERE is_success = 0 AND request_time > ?',
      [testStartTime]
    );

    // 按设备统计
    const machines = await queryBusiness(
      'SELECT id, device_code, name FROM cnc_machine WHERE collect_address_id = 1'
    );
    let deviceStats = '';
    for (const m of machines) {
      const segs = await queryBusiness(
        `SELECT program_name, SUM(quantity) as total_qty, COUNT(*) as seg_count
         FROM cnc_production_segment WHERE machine_id = ? AND created_at > ?
         GROUP BY program_name`,
        [m.id, testStartTime]
      );
      const totalQty = segs.reduce((s: number, r: any) => s + (Number(r.total_qty) || 0), 0);
      const programs = segs.map((r: any) => `${r.program_name}(${r.total_qty}个/${r.seg_count}段)`).join(', ');
      deviceStats += `| ${m.device_code} | ${m.name} | ${totalQty} | ${segs.length} | ${programs} |\n`;
    }

    // 阶段时间表
    let phaseTable = '';
    const phaseNames: Record<string, string> = {
      'normal1_start': '阶段1: 正常采集开始',
      'http500_start': '阶段2: HTTP 500异常开始',
      'http500_end': '阶段2: HTTP 500异常结束',
      'timeout_start': '阶段3: 超时异常开始',
      'timeout_end': '阶段3: 超时异常结束',
      'empty_start': '阶段4: 空数据返回开始',
      'empty_end': '阶段4: 空数据返回结束',
      'refuse_start': '阶段5: 拒绝连接开始',
      'refuse_end': '阶段5: 拒绝连接结束',
      'normal2_start': '阶段6: 恢复正常开始',
      'normal2_end': '阶段6: 恢复正常结束',
    };
    for (const [key, name] of Object.entries(phaseNames)) {
      const ts = phaseTimestamps[key];
      if (ts) {
        phaseTable += `| ${name} | ${ts.toLocaleString('zh-CN')} |\n`;
      }
    }

    // 事件历史摘要
    let eventSummary = '';
    if (Array.isArray(eventHistoryData) && eventHistoryData.length > 0) {
      const eventTypeCounts: Record<string, number> = {};
      for (const evt of eventHistoryData) {
        const type = evt.eventType || 'unknown';
        eventTypeCounts[type] = (eventTypeCounts[type] || 0) + 1;
      }
      for (const [type, count] of Object.entries(eventTypeCounts)) {
        eventSummary += `| ${type} | ${count} |\n`;
      }
    }

    // 异常日志摘要
    let errorSummary = '';
    if (Array.isArray(errorLogData) && errorLogData.length > 0) {
      for (const err of errorLogData.slice(0, 30)) {
        errorSummary += `| ${err.timestamp} | ${err.errorType} | ${err.description} | ${err.affectedDevices} |\n`;
      }
    }

    const report = `# 15分钟自动化采集测试报告

**生成时间:** ${new Date().toLocaleString('zh-CN')}
**测试开始:** ${testStartTime.toLocaleString('zh-CN')}
**测试结束:** ${new Date().toLocaleString('zh-CN')}
**总时长:** ${Math.round((Date.now() - testStartTime.getTime()) / 60000)}分钟

---

## 一、测试环境

| 项目 | 值 |
|------|------|
| 模拟器网关 | ${SIM_GATEWAY} |
| 模拟地址端口 | ${simPort} |
| 采集间隔 | 10秒 |
| 机床数量 | ${machines.length}台 |
| 采集地址 | http://localhost:${simPort}/ (id=1) |

## 二、测试阶段时间表

| 阶段 | 时间 |
|------|------|
| 测试开始 | ${testStartTime.toLocaleString('zh-CN')} |
${phaseTable}
| 测试结束 | ${new Date().toLocaleString('zh-CN')} |

## 三、模拟器汇总数据

| 项目 | 值 |
|------|------|
| 总设备数 | ${simSummary.totalDevices || '?'} |
| 在线设备数 | ${simSummary.onlineDevices || '?'} |
| 总加工零件数 | ${simSummary.totalParts || '?'} |
| 总请求数 | ${simSummary.totalRequests || '?'} |
| 成功请求数 | ${simSummary.successRequests || '?'} |
| 失败请求数 | ${simSummary.failRequests || '?'} |
| 异常记录数 | ${simSummary.errorCount || 0} |

## 四、数据库统计

| 项目 | 数量 |
|------|------|
| 采集记录数(cnc_collect_record) | ${dbRecords[0].cnt} |
| 产量分段数(cnc_production_segment) | ${dbSegments[0].cnt} |
| 已结账分段数 | ${dbSettled[0].cnt} |
| 原始日志数(log_collect_raw) | ${dbRawLogs[0].cnt} |
| 成功日志数 | ${dbSuccessLogs[0].cnt} |
| 失败日志数 | ${dbFailLogs[0].cnt} |

## 五、按设备产量对比

| 设备编码 | 设备名称 | 数据库总产量 | 分段数 | 程序明细 |
|---------|---------|-------------|--------|---------|
${deviceStats}

## 六、事件历史统计

| 事件类型 | 次数 |
|---------|------|
${eventSummary || '| (无事件) | 0 |'}

## 七、异常日志记录

| 时间 | 类型 | 描述 | 影响设备数 |
|------|------|------|-----------|
${errorSummary || '| (无异常记录) | - | - | - |'}

## 八、结论

${globalMismatchCount === 0 ? '✅ 所有设备零件数量完全匹配，数据采集准确性验证通过。' : `⚠️ 有 ${globalMismatchCount} 台设备零件数存在差异，需进一步排查。`}

- 采集记录总数: ${dbRecords[0].cnt}
- 产量分段总数: ${dbSegments[0].cnt} (已结账 ${dbSettled[0].cnt})
- 原始日志总数: ${dbRawLogs[0].cnt} (成功 ${dbSuccessLogs[0].cnt}, 失败 ${dbFailLogs[0].cnt})
- 异常恢复验证: ${phaseTimestamps['normal2_start'] ? '✅ 恢复后数据正常产生' : '❌ 未验证'}

---
*报告由 Playwright 自动化测试生成*
`;

    fs.writeFileSync(reportPath, report, 'utf-8');
    log(`\n===== 测试报告已生成 =====`);
    log(`  路径: ${reportPath}`);
    log(`\n===== 15分钟自动化采集测试结束 =====\n`);
  });

});
