#!/usr/bin/env node
/**
 * 压测脚本 - 模拟 300 人并发访问 https://zhenyuskill.com
 * 运行: node deploy/stress-test.js
 */

const https = require('https')
const { URL } = require('url')
const fs = require('fs')
const path = require('path')

const BASE_URL = 'https://zhenyuskill.com'

// ─── 工具函数 ───

function percentile(sorted, p) {
  if (!sorted.length) return 0
  const idx = Math.ceil(sorted.length * p / 100) - 1
  return sorted[Math.max(0, idx)]
}

function makeRequest(urlStr, options = {}) {
  return new Promise((resolve) => {
    const start = Date.now()
    const url = new URL(urlStr)
    const reqOpts = {
      hostname: url.hostname,
      port: url.port || 443,
      path: url.pathname + url.search,
      method: options.method || 'GET',
      headers: options.headers || {},
      timeout: 15000,
    }

    const req = https.request(reqOpts, (res) => {
      let body = ''
      res.on('data', (chunk) => { body += chunk })
      res.on('end', () => {
        resolve({
          status: res.statusCode,
          latency: Date.now() - start,
          success: res.statusCode >= 200 && res.statusCode < 400,
        })
      })
    })

    req.on('error', () => {
      resolve({ status: 0, latency: Date.now() - start, success: false })
    })

    req.on('timeout', () => {
      req.destroy()
      resolve({ status: 0, latency: Date.now() - start, success: false })
    })

    if (options.body) {
      req.write(options.body)
    }
    req.end()
  })
}

async function runScenario({ name, url, method, headers, bodyFn, concurrency, durationSec }) {
  console.log(`\n${'═'.repeat(60)}`)
  console.log(`  场景: ${name}`)
  console.log(`  目标: ${url}`)
  console.log(`  并发: ${concurrency} | 持续: ${durationSec}s`)
  console.log(`${'═'.repeat(60)}`)

  const results = []
  let running = true
  let activeRequests = 0

  const endTime = Date.now() + durationSec * 1000

  function worker() {
    return new Promise(async (resolve) => {
      while (running && Date.now() < endTime) {
        activeRequests++
        const opts = { method: method || 'GET', headers: { ...(headers || {}) } }
        if (bodyFn) {
          const payload = JSON.stringify(bodyFn())
          opts.headers['Content-Type'] = 'application/json'
          opts.body = payload
        }
        const result = await makeRequest(url, opts)
        results.push(result)
        activeRequests--
      }
      resolve()
    })
  }

  // 启动所有并发 worker
  const workers = []
  for (let i = 0; i < concurrency; i++) {
    workers.push(worker())
  }

  // 进度打印
  const progressInterval = setInterval(() => {
    const elapsed = Math.round((Date.now() - (endTime - durationSec * 1000)) / 1000)
    process.stdout.write(`\r  进度: ${elapsed}s/${durationSec}s | 完成请求: ${results.length} | 活跃连接: ${activeRequests}`)
  }, 1000)

  await Promise.all(workers)
  running = false
  clearInterval(progressInterval)
  process.stdout.write('\r' + ' '.repeat(80) + '\r')

  // 统计
  const totalRequests = results.length
  const successCount = results.filter(r => r.success).length
  const failCount = totalRequests - successCount
  const latencies = results.map(r => r.latency).sort((a, b) => a - b)
  const totalTime = durationSec
  const qps = (totalRequests / totalTime).toFixed(2)
  const errorRate = totalRequests > 0 ? ((failCount / totalRequests) * 100).toFixed(2) : '0.00'

  const stats = {
    scenario: name,
    url,
    concurrency,
    durationSec,
    totalRequests,
    successCount,
    failCount,
    qps: parseFloat(qps),
    errorRate: parseFloat(errorRate),
    latency: {
      min: latencies[0] || 0,
      max: latencies[latencies.length - 1] || 0,
      avg: totalRequests > 0 ? Math.round(latencies.reduce((a, b) => a + b, 0) / totalRequests) : 0,
      p50: percentile(latencies, 50),
      p95: percentile(latencies, 95),
      p99: percentile(latencies, 99),
    }
  }

  // 输出表格
  console.log(`  ┌─────────────────────────────────────────────────┐`)
  console.log(`  │ 总请求数:  ${String(stats.totalRequests).padEnd(10)} │ QPS:       ${String(stats.qps).padEnd(10)} │`)
  console.log(`  │ 成功:      ${String(stats.successCount).padEnd(10)} │ 失败:      ${String(stats.failCount).padEnd(10)} │`)
  console.log(`  │ 错误率:    ${(stats.errorRate + '%').padEnd(10)} │                       │`)
  console.log(`  ├─────────────────────────────────────────────────┤`)
  console.log(`  │ 响应时间 (ms)                                   │`)
  console.log(`  │   Min: ${String(stats.latency.min).padEnd(8)} Max: ${String(stats.latency.max).padEnd(8)} Avg: ${String(stats.latency.avg).padEnd(8)} │`)
  console.log(`  │   P50: ${String(stats.latency.p50).padEnd(8)} P95: ${String(stats.latency.p95).padEnd(8)} P99: ${String(stats.latency.p99).padEnd(8)} │`)
  console.log(`  └─────────────────────────────────────────────────┘`)

  return stats
}

// ─── 压测场景定义 ───

// 创建技能的请求体生成器（每次不同数据）
let createCounter = 0
function createSkillBody() {
  createCounter++
  return {
    name: `压测技能_${Date.now()}_${createCounter}`,
    description: `自动化压测创建的技能 #${createCounter}`,
    node_id: 'cat_root',  // 使用一个存在的分类节点
    source: 'create',
    creator_name: `压测用户${createCounter % 100}`,
  }
}

const scenarios = [
  {
    name: '首页加载 (GET /)',
    url: `${BASE_URL}/`,
    method: 'GET',
    concurrency: 300,
    durationSec: 30,
  },
  {
    name: '技能列表浏览 (GET /api/skills)',
    url: `${BASE_URL}/api/skills?page=1&size=20`,
    method: 'GET',
    concurrency: 300,
    durationSec: 30,
  },
  {
    name: '分类树加载 (GET /api/categories/tree)',
    url: `${BASE_URL}/api/categories/tree`,
    method: 'GET',
    concurrency: 300,
    durationSec: 30,
  },
  {
    name: '创建技能 (POST /api/skills)',
    url: `${BASE_URL}/api/skills`,
    method: 'POST',
    headers: {
      'X-User-Email': 'stresstest@zhenyuskill.com',
      'X-User-Phone': '13800000000',
    },
    bodyFn: createSkillBody,
    concurrency: 100,
    durationSec: 30,
  },
]

// ─── 主流程 ───

async function main() {
  console.log('\n')
  console.log('╔══════════════════════════════════════════════════════════════╗')
  console.log('║           zhenyuskill.com 压力测试                          ║')
  console.log('║           目标: https://zhenyuskill.com                     ║')
  console.log('║           场景: 4 个 | 最大并发: 300                        ║')
  console.log('╚══════════════════════════════════════════════════════════════╝')
  console.log(`\n  开始时间: ${new Date().toLocaleString('zh-CN')}`)

  const allStats = []

  for (const scenario of scenarios) {
    const stats = await runScenario(scenario)
    allStats.push(stats)
    // 场景间休息 2 秒，让服务器恢复
    await new Promise(r => setTimeout(r, 2000))
  }

  // ─── 汇总表格 ───
  console.log('\n\n')
  console.log('╔══════════════════════════════════════════════════════════════════════════════════════╗')
  console.log('║                              压测结果汇总                                          ║')
  console.log('╠════════════════════════════════╦═══════╦═══════╦═══════╦═══════╦═══════╦═══════════╣')
  console.log('║ 场景                           ║ QPS   ║ 成功  ║ 失败  ║ P50ms ║ P99ms ║ 错误率   ║')
  console.log('╠════════════════════════════════╬═══════╬═══════╬═══════╬═══════╬═══════╬═══════════╣')

  for (const s of allStats) {
    const name = s.scenario.slice(0, 28).padEnd(28)
    const qps = String(s.qps).padEnd(5)
    const success = String(s.successCount).padEnd(5)
    const fail = String(s.failCount).padEnd(5)
    const p50 = String(s.latency.p50).padEnd(5)
    const p99 = String(s.latency.p99).padEnd(5)
    const err = (s.errorRate + '%').padEnd(9)
    console.log(`║ ${name} ║ ${qps} ║ ${success} ║ ${fail} ║ ${p50} ║ ${p99} ║ ${err} ║`)
  }

  console.log('╚════════════════════════════════╩═══════╩═══════╩═══════╩═══════╩═══════╩═══════════╝')
  console.log(`\n  结束时间: ${new Date().toLocaleString('zh-CN')}`)

  // ─── 保存报告 ───
  const report = {
    target: BASE_URL,
    startedAt: new Date().toISOString(),
    scenarios: allStats,
    summary: {
      totalRequests: allStats.reduce((s, x) => s + x.totalRequests, 0),
      totalSuccess: allStats.reduce((s, x) => s + x.successCount, 0),
      totalFail: allStats.reduce((s, x) => s + x.failCount, 0),
      avgQps: parseFloat((allStats.reduce((s, x) => s + x.qps, 0) / allStats.length).toFixed(2)),
      avgErrorRate: parseFloat((allStats.reduce((s, x) => s + x.errorRate, 0) / allStats.length).toFixed(2)),
    }
  }

  const reportPath = path.join(__dirname, 'stress-test-report.json')
  fs.writeFileSync(reportPath, JSON.stringify(report, null, 2), 'utf-8')
  console.log(`\n  报告已保存: ${reportPath}`)
  console.log('\n')
}

main().catch(err => {
  console.error('压测执行失败:', err)
  process.exit(1)
})
