/**
 * 快手行业电子凭证接口 HTTP 测试 & 压力测试脚本
 *
 * 用法：
 *   # 本地测试
 *   npm run test:industry:http
 *   npm run test:industry:http -- --baseUrl=http://127.0.0.1:3000
 *
 *   # 远程测试（生产/测试环境）
 *   npm run test:industry:http -- --provider=production
 *   npm run test:industry:http -- --baseUrl=https://xxx.xxx.com --appKey=xxx --signSecret=xxx
 *
 *   # 压力测试
 *   npm run test:industry:http -- --load --tps=100 --duration=10
 *   npm run test:industry:http -- --baseUrl=https://xxx.xxx.com --load --endpoint=query-code --tps=500 --duration=30
 *
 * 前提：
 *   1. 目标服务已启动
 *   2. .env 中配置了 KUASHOU_INDUSTRY_APP_KEY 和 KUASHOU_INDUSTRY_SIGN_SECRET
 *      （可通过 --appKey 和 --signSecret 覆盖）
 *   3. 压力测试前需调高限流阈值 (KUASHOU_INDUSTRY_RATE_LIMIT_MAX=60000)
 */

import crypto from 'node:crypto'
import path from 'node:path'
import process from 'node:process'
import { fileURLToPath } from 'node:url'

import { loadEnvFiles } from '../src/config/runtime-env.js'

type JsonObject = Record<string, unknown>

const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url))
const PROJECT_ROOT = path.resolve(CURRENT_DIR, '..')
const WORKSPACE_ROOT = path.resolve(PROJECT_ROOT, '../..')

loadEnvFiles([path.join(WORKSPACE_ROOT, '.env'), path.join(PROJECT_ROOT, '.env')])

const args = parseArgs(process.argv.slice(2))

// 环境预设
const providerPresets: Record<string, { baseUrl: string; description: string }> = {
  production: {
    baseUrl: 'https://openapi.kwaixiaodian.com',
    description: '快手开放平台线上环境',
  },
  'production-backup': {
    baseUrl: 'https://open.kwaixiaodian.com',
    description: '快手开放平台线上环境（备用）',
  },
  staging: {
    baseUrl: 'https://gw-merchant-staging.test.gifshow.com',
    description: '快手开放平台测试环境',
  },
}

let baseUrl: string
if (args.provider && providerPresets[String(args.provider)]) {
  const preset = providerPresets[String(args.provider)]
  baseUrl = String(args.baseUrl || preset.baseUrl).replace(/\/+$/, '')
  if (!args.baseUrl) {
    console.log(`  预设环境: ${preset.description}`)
  }
} else {
  baseUrl = String(
    args.baseUrl || process.env.KUASHOU_INDUSTRY_TEST_BASE_URL || 'http://127.0.0.1:3000',
  ).replace(/\/+$/, '')
}

// 签名配置：CLI 参数 > 环境变量 > 默认值
const appKey = (String(args.appKey || '') || process.env.KUASHOU_INDUSTRY_APP_KEY || '').trim()
const signSecret = (
  String(args.signSecret || '') ||
  process.env.KUASHOU_INDUSTRY_SIGN_SECRET ||
  ''
).trim()
const signMethod = String(args.signMethod || 'MD5')
  .trim()
  .toUpperCase()

const isLoadTest = Boolean(args.load)
const targetTps = normalizePositiveInteger(args.tps, 100)
const testDuration = normalizePositiveInteger(args.duration, 10)
const testEndpoint = String(args.endpoint || '').trim()
const insecure = Boolean(args.insecure) // 跳过 SSL 证书验证（仅测试环境）

if (args.help) {
  printHelp()
  process.exit(0)
}

if (!appKey || !signSecret) {
  console.error('缺少签名配置。请通过以下任一方式提供：')
  console.error('  1. .env 中设置 KUASHOU_INDUSTRY_APP_KEY 和 KUASHOU_INDUSTRY_SIGN_SECRET')
  console.error('  2. 命令行: --appKey=xxx --signSecret=xxx')
  process.exit(1)
}

if (insecure) {
  process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
  console.log('  ⚠ SSL 证书验证已跳过（--insecure）')
}

// ─────────────────────────────────────────────────────────────
// 功能测试
// ─────────────────────────────────────────────────────────────
if (!isLoadTest) {
  const testOid = `HTTP_TEST_${Date.now()}`
  const testNum = 3

  console.log('')
  console.log('╔══════════════════════════════════════════════════════╗')
  console.log('║   快手行业电子凭证 HTTP 接口功能测试                  ║')
  console.log('╠══════════════════════════════════════════════════════╣')
  console.log(`║  服务地址   : ${baseUrl.padEnd(36)}║`)
  console.log(`║  appKey     : ${appKey.padEnd(36)}║`)
  console.log(`║  signMethod : ${signMethod.padEnd(36)}║`)
  console.log(`║  测试订单号 : ${testOid.padEnd(36)}║`)
  console.log(`║  发码数量   : ${String(testNum).padEnd(36)}║`)
  console.log('╚══════════════════════════════════════════════════════╝')
  console.log('')

  let passed = 0
  let failed = 0

  // 1. 通知商家发码
  const sendParams = buildSignedParams(
    appKey,
    {
      oid: testOid,
      sellerId: '2174425348',
      num: testNum,
      itemId: '20842040339391',
      skuId: '87457167264391',
      itemTitle: '测试电子凭证商品',
      sendType: 'VIRTUAL',
      eticketType: 'DINING_OPEN_TICKET',
      token: `test-token-${testOid}`,
      certExpireType: 3,
      certExpDays: 30,
      certActualStartTime: Date.now(),
      certActualEndTime: Date.now() + 30 * 24 * 60 * 60 * 1000,
    },
    signMethod,
  )

  const r1 = await post('/send-code', sendParams, { oid: testOid })
  if (assertResult(1, '通知商家发码', r1)) passed++
  else failed++
  if (r1.ok) {
    const len = r1.data?.data?.etickets?.length || 0
    const firstId = r1.data?.data?.etickets?.[0]?.id
    console.log(`       返回卡券数: ${len}，首张 ID: ${firstId}`)
  }

  // 2. 重复发码（幂等性）
  const r2 = await post('/send-code', sendParams, { oid: testOid })
  if (assertResult(2, '重复发码（幂等性）', r2)) passed++
  else failed++

  // 3. 查询全部卡券
  const qBiz: JsonObject = { oid: testOid, sendType: 'VIRTUAL', eticketType: 'DINING_OPEN_TICKET' }
  const r3 = await post('/query-code', buildSignedParams(appKey, qBiz, signMethod), {
    oid: testOid,
  })
  if (assertResult(3, '查询全部卡券', r3)) passed++
  else failed++
  if (r3.ok) {
    console.log(`       卡券数: ${r3.data?.data?.etickets?.length || 0}`)
    r3.data?.data?.etickets?.forEach((e: JsonObject, i: number) => {
      console.log(`       [${i + 1}] id=${e.id} status=${e.status}`)
    })
  }

  // 4. 查询单个卡券
  const firstEticketId = r3.data?.data?.etickets?.[0]?.id
  if (firstEticketId) {
    const r4 = await post(
      '/query-code',
      buildSignedParams(
        appKey,
        {
          oid: testOid,
          eticketId: String(firstEticketId),
          sendType: 'VIRTUAL',
          eticketType: 'DINING_OPEN_TICKET',
        },
        signMethod,
      ),
      { oid: testOid },
    )
    if (assertResult(4, `查询单个卡券 (eticketId=${firstEticketId})`, r4)) passed++
    else failed++
  }

  // 5. 查询不存在的订单
  const fakeOid = `FAKE_${Date.now()}`
  const r5 = await post(
    '/query-code',
    buildSignedParams(appKey, { oid: fakeOid, sendType: 'VIRTUAL' }, signMethod),
    { oid: fakeOid },
  )
  const expect4012002 = r5.data?.result === 4012002
  if (expect4012002) {
    passed++
    console.log(`  ✅ [5] 查询不存在订单: result=4012002 (符合预期)`)
  } else {
    failed++
    console.log(`  ❌ [5] 查询不存在订单: 期望 4012002 实际 ${r5.data?.result}`)
  }

  // 6. 销毁指定卡券
  if (firstEticketId) {
    const r6 = await post(
      '/destroy-code',
      buildSignedParams(
        appKey,
        {
          oid: testOid,
          reason: 'USER_APPLY_REFUND',
          etickets: [{ id: String(firstEticketId), num: 1, goodsValue: 100 }],
        },
        signMethod,
      ),
      { oid: testOid },
    )
    if (assertResult(6, `销毁卡券 (id=${firstEticketId})`, r6)) passed++
    else failed++

    // 7. 验证销毁后状态
    const r7 = await post(
      '/query-code',
      buildSignedParams(
        appKey,
        { oid: testOid, eticketId: String(firstEticketId), sendType: 'VIRTUAL' },
        signMethod,
      ),
      { oid: testOid },
    )
    const destroyed = r7.data?.result === 1 && r7.data?.data?.etickets?.[0]?.status === 'DESTROYED'
    if (destroyed) {
      passed++
      console.log(`  ✅ [7] 销毁后验证: status=DESTROYED`)
    } else {
      failed++
      console.log(
        `  ❌ [7] 销毁后验证: 期望 DESTROYED 实际 ${r7.data?.data?.etickets?.[0]?.status}`,
      )
    }
  }

  // 8. 整单销毁
  const r8 = await post(
    '/destroy-code',
    buildSignedParams(appKey, { oid: testOid, reason: 'ETICKET_EXPIRED' }, signMethod),
    { oid: testOid },
  )
  if (assertResult(8, '整单销毁', r8)) passed++
  else failed++

  // 9. 签名错误测试
  const badParams = buildSignedParams(appKey, { oid: testOid, sendType: 'VIRTUAL' }, signMethod)
  badParams.sign = 'bad_sign_value'
  const r9 = await post('/query-code', badParams, { oid: testOid })
  const signRejected = r9.status === 400 || (r9.data?.result && r9.data?.result !== 1)
  if (signRejected) {
    passed++
    console.log(`  ✅ [9] 签名错误被拒绝: HTTP ${r9.status}`)
  } else {
    failed++
    console.log(`  ❌ [9] 签名错误未被拒绝`)
  }

  console.log('')
  console.log('╔══════════════════════════════════════════════════════╗')
  console.log(
    `║  HTTP 功能测试: ${String(passed).padStart(2)}/${passed + failed} 通过, ${String(failed).padStart(2)}/${passed + failed} 失败              ║`,
  )
  console.log('╚══════════════════════════════════════════════════════╝')
  console.log('')
  process.exit(failed > 0 ? 1 : 0)
}

// ─────────────────────────────────────────────────────────────
// 压力测试
// ─────────────────────────────────────────────────────────────
const endpointsToTest = testEndpoint ? [testEndpoint] : ['send-code', 'query-code', 'destroy-code']

const loadTestOid = `LOAD_${Date.now()}`

// 先发码创建订单以供查询/销毁
console.log('')
console.log(`  准备测试数据: 通知商家发码 (oid=${loadTestOid})...`)
const prepParams = buildSignedParams(
  appKey,
  {
    oid: loadTestOid,
    sellerId: '2174425348',
    num: 10,
    itemId: '20842040339391',
    skuId: '87457167264391',
    itemTitle: '压测商品',
    sendType: 'VIRTUAL',
    eticketType: 'DINING_OPEN_TICKET',
    token: `load-test-token-${loadTestOid}`,
    certExpireType: 3,
    certExpDays: 30,
    certActualStartTime: Date.now(),
    certActualEndTime: Date.now() + 30 * 24 * 60 * 60 * 1000,
  },
  signMethod,
)
const prepResult = await post('/send-code', prepParams, {})
if (prepResult.data?.result === 1) {
  console.log(`  数据准备完成 (${prepResult.ms}ms)`)
} else {
  console.log(
    `  ⚠ 数据准备失败: result=${prepResult.data?.result} error_msg="${prepResult.data?.error_msg}" (${prepResult.ms}ms)`,
  )
  console.log(`  请确认服务端已配置正确的 appKey/signSecret 且未触发限流`)
}

for (const endpoint of endpointsToTest) {
  console.log('')
  console.log('╔══════════════════════════════════════════════════════╗')
  console.log(`║  压测: ${endpoint.padEnd(46)}║`)
  console.log(
    `║  目标: ${String(targetTps).padEnd(4)} TPS, 持续 ${String(testDuration).padEnd(3)}s                        ║`,
  )
  console.log('╚══════════════════════════════════════════════════════╝')

  const result = await runLoadTest(endpoint, targetTps, testDuration, loadTestOid)
  console.log(`  成功 TPS    : ${result.successTps.toFixed(1)} (业务 result=1)`)
  console.log(`  总请求数    : ${result.totalReqs}`)
  console.log(`  业务成功    : ${result.successes}`)
  console.log(
    `  业务失败    : ${result.bizErrors}${result.bizSample ? ` (示例: ${result.bizSample})` : ''}`,
  )
  console.log(`  被限流      : ${result.rateLimited}`)
  console.log(`  平均耗时    : ${result.avgMs.toFixed(1)} ms`)
  console.log(`  P50         : ${result.p50Ms.toFixed(1)} ms`)
  console.log(`  P95         : ${result.p95Ms.toFixed(1)} ms`)
  console.log(`  P99         : ${result.p99Ms.toFixed(1)} ms`)
  console.log(`  最小耗时    : ${result.minMs.toFixed(1)} ms`)
  console.log(`  最大耗时    : ${result.maxMs.toFixed(1)} ms`)

  if (result.rateLimited > result.totalReqs * 0.1) {
    console.log(
      `  ⚠ 注意: ${((result.rateLimited / result.totalReqs) * 100).toFixed(0)}% 请求被限流，建议调高 KUASHOU_INDUSTRY_RATE_LIMIT_MAX`,
    )
  }
}

console.log('')

// ─────────────────── 核心函数 ───────────────────

async function post(
  path: string,
  params: JsonObject,
  _ctx: JsonObject,
): Promise<{ ok: boolean; status: number; data: JsonObject; ms: number }> {
  const url = `${baseUrl}/api/v1/open/kuaishou-industry${path}`
  const body = new URLSearchParams()

  for (const [key, value] of Object.entries(params)) {
    if (key !== 'param') {
      body.append(key, String(value))
    }
  }
  body.append('param', String(params.param || '{}'))

  const startedAt = Date.now()
  let status = 0
  let data: JsonObject
  let ok = false

  try {
    const res = await fetch(url, {
      method: 'POST',
      headers: { 'content-type': 'application/x-www-form-urlencoded' },
      body: body.toString(),
    })
    status = res.status
    const text = await res.text()
    try {
      data = JSON.parse(text)
    } catch {
      data = { raw: text }
    }
    ok = res.ok && data.result === 1
  } catch (err) {
    data = { error: String(err) }
  }

  return { ok, status, data, ms: Date.now() - startedAt }
}

async function runLoadTest(
  endpoint: string,
  tps: number,
  duration: number,
  oid: string,
): Promise<{
  successTps: number
  totalReqs: number
  successes: number
  bizErrors: number
  bizSample: string
  rateLimited: number
  avgMs: number
  p50Ms: number
  p95Ms: number
  p99Ms: number
  minMs: number
  maxMs: number
}> {
  const latencies: number[] = []
  let successes = 0
  let bizErrors = 0
  let rateLimited = 0
  let bizSample = ''
  const startedAt = Date.now()
  const deadline = startedAt + duration * 1000
  const intervalMs = Math.max(1, Math.floor(1000 / tps))

  const worker = async () => {
    while (Date.now() < deadline) {
      const startedReq = Date.now()

      const bizParams: JsonObject = {
        oid,
        sendType: 'VIRTUAL',
        eticketType: 'DINING_OPEN_TICKET',
      }

      if (endpoint === 'send-code') {
        bizParams.sellerId = '2174425348'
        bizParams.num = 1
        bizParams.itemId = '20842040339391'
        bizParams.token = `load-${Date.now()}`
        bizParams.certExpireType = 3
        bizParams.certExpDays = 30
        bizParams.certActualStartTime = Date.now()
        bizParams.certActualEndTime = Date.now() + 30 * 24 * 60 * 60 * 1000
      }

      if (endpoint === 'destroy-code') {
        bizParams.reason = 'ETICKET_EXPIRED'
      }

      const params = buildSignedParams(appKey, bizParams, signMethod)
      const { ok, data, ms } = await post(`/${endpoint}`, params, { oid })

      latencies.push(ms)

      if (ok) {
        successes++
      } else if (data?.error_msg && String(data.error_msg).includes('频繁')) {
        rateLimited++
      } else if (data?.result === 4010003 && String(data?.error_msg || '').includes('频繁')) {
        rateLimited++
      } else if (data?.result != null && data?.result !== 1) {
        bizErrors++
        if (!bizSample) {
          bizSample = `result=${data.result} "${data.error_msg}"`
        }
      } else {
        bizErrors++
        if (!bizSample) {
          bizSample = `HTTP ${ms}ms ${JSON.stringify(data).slice(0, 100)}`
        }
      }

      const elapsed = Date.now() - startedReq
      const sleep = intervalMs - elapsed
      if (sleep > 0) {
        await delay(sleep)
      }
    }
  }

  const concurrency = Math.max(1, Math.ceil(tps / 50))
  const workers = Array.from({ length: concurrency }, () => worker())
  await Promise.all(workers)

  latencies.sort((a, b) => a - b)

  return {
    successTps: successes / Math.max(1, (Date.now() - startedAt) / 1000),
    totalReqs: latencies.length,
    successes,
    bizErrors,
    bizSample,
    rateLimited,
    avgMs: avg(latencies),
    p50Ms: percentile(latencies, 50),
    p95Ms: percentile(latencies, 95),
    p99Ms: percentile(latencies, 99),
    minMs: latencies[0] || 0,
    maxMs: latencies[latencies.length - 1] || 0,
  }
}

// ─────────────────── 签名 ───────────────────

function buildSignedParams(
  currentAppKey: string,
  bizParams: JsonObject,
  method: string,
): JsonObject {
  const paramStr = JSON.stringify(bizParams)

  const params: JsonObject = {
    appkey: currentAppKey,
    version: '1',
    timestamp: Date.now(),
    signMethod: method,
    access_token: `test-token-${Date.now()}`,
    method: 'integration.virtual.eticket.callback',
    param: paramStr,
  }

  const sign = signKuaishouIndustry(params, method)
  params.sign = sign
  return params
}

function signKuaishouIndustry(params: JsonObject, method: string): string {
  const entries = Object.entries(params)
    .filter(([key]) => key !== 'sign' && key !== 'signSecret')
    .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))

  const queryString = entries.map(([key, value]) => `${key}=${stringifySignVal(value)}`).join('&')

  const source = `${queryString}&signSecret=${signSecret}`

  if (method === 'HMAC_SHA256') {
    return crypto.createHmac('sha256', signSecret).update(source, 'utf8').digest('base64')
  }

  return crypto.createHash('md5').update(source, 'utf8').digest('hex').toLowerCase()
}

function stringifySignVal(value: unknown): string {
  if (typeof value === 'number' && Number.isFinite(value)) return String(value)
  if (typeof value === 'boolean') return value ? 'true' : 'false'
  if (value == null) return ''
  if (typeof value === 'object')
    return JSON.stringify(value, Object.keys(value as JsonObject).sort())
  return String(value)
}

// ─────────────────── 工具 ───────────────────

function assertResult(
  num: number,
  label: string,
  r: { ok: boolean; status: number; data: JsonObject; ms: number },
) {
  if (r.ok) {
    console.log(`  ✅ [${num}] ${label}: result=1 (${r.ms}ms)`)
    return true
  }
  console.log(
    `  ❌ [${num}] ${label}: HTTP ${r.status} result=${r.data?.result} error_msg="${r.data?.error_msg}" (${r.ms}ms)`,
  )
  return false
}

function delay(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms))
}

function avg(arr: number[]): number {
  if (arr.length === 0) return 0
  return arr.reduce((s, v) => s + v, 0) / arr.length
}

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

function normalizePositiveInteger(value: unknown, fallback: number): number {
  const parsed = Number(value)
  return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback
}

function parseArgs(rawArgs: string[]): JsonObject {
  const parsed: JsonObject = {}
  for (const rawArg of rawArgs) {
    const arg = String(rawArg || '').trim()
    if (!arg) continue
    if (arg === '--help' || arg === '-h') {
      parsed.help = true
      continue
    }
    const normalized = arg.startsWith('--') ? arg.slice(2) : arg
    const eqIdx = normalized.indexOf('=')
    if (eqIdx < 0) {
      parsed[normalized] = true
      continue
    }
    const key = normalized.slice(0, eqIdx).trim()
    const value = normalized.slice(eqIdx + 1).trim()
    if (key) parsed[key] = value
  }
  return parsed
}

function printHelp() {
  console.log(`
快手行业电子凭证 HTTP 测试 & 压力测试

功能测试:
  npm run test:industry:http
  npm run test:industry:http -- --baseUrl=http://127.0.0.1:3000
  npm run test:industry:http -- --provider=production
  npm run test:industry:http -- --baseUrl=https://xxx.xxx.com --appKey=xxx --signSecret=xxx

压力测试:
  npm run test:industry:http -- --load --tps=100 --duration=10
  npm run test:industry:http -- --baseUrl=https://xxx.xxx.com --load --endpoint=query-code --tps=500 --duration=30

参数:
  --baseUrl      目标服务地址，默认 http://127.0.0.1:3000
  --provider     环境预设: production / production-backup / staging
  --appKey       快手分配的 appKey（覆盖 .env）
  --signSecret   签名密钥（覆盖 .env）
  --signMethod   签名算法 MD5 或 HMAC_SHA256，默认 MD5
  --load         启用压力测试模式
  --tps          目标 TPS/QPS，默认 100
  --duration     持续时间（秒），默认 10
  --endpoint     指定测试接口: send-code / query-code / destroy-code，不指定则全部测试
  --insecure     跳过 SSL 证书验证（仅限测试环境）

远程测试示例:
  # 测试生产环境（使用 .env 中的生产密钥）
  npm run test:industry:http -- --provider=production

  # 测试测试环境（白名单 IP 需要提前配置）
  npm run test:industry:http -- --provider=staging --insecure

  # 自定义远程地址
  npm run test:industry:http -- --baseUrl=https://order.khhao.com

  # 远程压力测试
  npm run test:industry:http -- --baseUrl=https://order.khhao.com --load --tps=100 --duration=30

注意:
  - 测试环境需要将出口 IP 加入快手白名单
  - 压力测试前需调高限流阈值（KUASHOU_INDUSTRY_RATE_LIMIT_MAX=60000）
  - 远程压力测试注意不要影响线上业务
  - 签名密钥请勿提交到 git，建议通过环境变量或命令行传入
`)
}
