// 隧道中继本地冒烟测试（临时脚本，验证 server.js 的隧道链路；同时是 Rust 桌面端的协议参照）
// 自包含：拉起中继子进程 + 本地目标服务 + 隧道客户端，跑断言后退出。
// 运行：node _smoke.js
'use strict'
const http = require('http')
const net = require('net')
const { spawn } = require('child_process')
const WebSocket = require('ws')
const { WebSocketServer } = require('ws')

const BASE = 'example.test'
const SECRET = 'testsecret'
const RELAY_PORT = 8099
// 前缀 -> 本地端口
const MAP = { demo1: 4011, demo2: 4012, echo: 4013, chunk: 4014 }

let failures = 0
const log = (...a) => console.log(...a)
const ok = (name) => log(`  PASS  ${name}`)
const bad = (name, detail) => {
  failures++
  log(`  FAIL  ${name} -- ${detail}`)
}

// 逐跳头
const HOP = new Set(['connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', 'te', 'trailer', 'transfer-encoding', 'upgrade'])

// 帧类型（与 server.js 的 F 对齐）
const F = {
  REQ_HEAD: 0x01, REQ_BODY: 0x02, REQ_END: 0x03, REQ_ABORT: 0x04,
  U_OPEN: 0x20, PAUSE: 0x30, RESUME: 0x31,
  RES_HEAD: 0x10, RES_BODY: 0x11, RES_END: 0x12, RES_ABORT: 0x13,
  U_DATA: 0x22, U_CLOSE: 0x23,
}
function frame(type, id, payload) {
  const h = Buffer.allocUnsafe(5)
  h[0] = type
  h.writeUInt32BE(id >>> 0, 1)
  return payload && payload.length ? Buffer.concat([h, payload]) : h
}

// ---------- 本地目标服务 ----------
function startTargets() {
  const servers = []
  // demo1 / demo2：带 Content-Length 的普通响应
  for (const name of ['demo1', 'demo2']) {
    const s = http.createServer((req, res) => {
      const body = `hello from ${name} path=${req.url}`
      res.writeHead(200, { 'Content-Type': 'text/plain', 'Content-Length': Buffer.byteLength(body) })
      res.end(body)
    })
    s.listen(MAP[name], '127.0.0.1')
    servers.push(s)
  }
  // chunk：分块（chunked）响应，验证桌面端 de-chunk
  const cs = http.createServer((req, res) => {
    res.writeHead(200, { 'Content-Type': 'text/plain' }) // 不带 Content-Length → Node 自动 chunked
    res.write('part-A;')
    res.write('part-B;')
    res.end('part-C')
  })
  cs.listen(MAP.chunk, '127.0.0.1')
  servers.push(cs)
  // echo：WebSocket 回显，验证升级穿透（模拟 dev server HMR）
  const wss = new WebSocketServer({ port: MAP.echo, host: '127.0.0.1' })
  wss.on('connection', (ws) => ws.on('message', (m) => ws.send('echo:' + m.toString())))
  servers.push(wss)
  return servers
}

// ---------- 隧道客户端（桌面端 Rust 的 JS 参照实现）----------
function startTunnelClient(onReady) {
  const ws = new WebSocket(`ws://127.0.0.1:${RELAY_PORT}${'/__ccgui_tunnel__'}`, { headers: { Host: BASE } })
  const streams = new Map() // streamId -> {socket, kind, phase, buf, mode, remaining}

  ws.on('open', () => {
    ws.send(JSON.stringify({
      type: 'tunnel-register', secret: SECRET, room: 'r', token: 't',
      tunnels: Object.keys(MAP).map((sub) => ({ sub, enabled: true })),
    }))
  })
  ws.on('message', (data, isBinary) => {
    if (!isBinary) {
      const m = JSON.parse(data.toString())
      if (m.type === 'tunnel-registered') onReady(m)
      else if (m.type === 'error') { bad('tunnel-register', m.message); onReady(m) }
      return
    }
    const type = data[0]
    const id = data.readUInt32BE(1)
    const payload = data.subarray(5)
    if (type === F.REQ_HEAD) {
      const h = JSON.parse(payload.toString())
      const port = MAP[h.sub]
      const sock = net.connect(port, '127.0.0.1')
      const st = { socket: sock, kind: 'http', phase: 'head', buf: Buffer.alloc(0), mode: null, remaining: 0 }
      streams.set(id, st)
      // 重建请求：改写 Host 为本地 + 强制 Connection: close（响应靠 EOF 定界，桌面无需复用连接）
      const lines = [`${h.method} ${h.url} HTTP/1.1`]
      for (const k in h.headers) {
        const lk = k.toLowerCase()
        if (lk === 'host' || lk === 'connection' || lk === 'proxy-connection') continue
        const v = h.headers[k]
        if (Array.isArray(v)) v.forEach((vv) => lines.push(`${k}: ${vv}`))
        else lines.push(`${k}: ${v}`)
      }
      lines.push(`Host: 127.0.0.1:${port}`)
      lines.push('Connection: close')
      sock.write(lines.join('\r\n') + '\r\n\r\n')
      sock.on('data', (c) => onLocalResp(id, st, c))
      sock.on('end', () => finishHttp(id, st))
      sock.on('close', () => finishHttp(id, st))
      sock.on('error', () => { send(F.RES_ABORT, id); streams.delete(id) })
    } else if (type === F.REQ_BODY) {
      const st = streams.get(id); if (st) st.socket.write(payload)
    } else if (type === F.REQ_END) {
      // Connection: close 已在头里；GET 无 body。POST 的 body 已随 REQ_BODY 写入。
    } else if (type === F.REQ_ABORT) {
      const st = streams.get(id); if (st) { try { st.socket.destroy() } catch {} ; streams.delete(id) }
    } else if (type === F.U_OPEN) {
      const h = JSON.parse(payload.toString())
      const port = MAP[h.sub]
      const sock = net.connect(port, '127.0.0.1')
      const st = { socket: sock, kind: 'raw' }
      streams.set(id, st)
      const lines = [`${h.method} ${h.url} HTTP/1.1`]
      for (const k in h.headers) {
        const lk = k.toLowerCase()
        if (lk === 'host') continue
        const v = h.headers[k]
        if (Array.isArray(v)) v.forEach((vv) => lines.push(`${k}: ${vv}`))
        else lines.push(`${k}: ${v}`)
      }
      lines.push(`Host: 127.0.0.1:${port}`)
      sock.write(lines.join('\r\n') + '\r\n\r\n')
      sock.on('data', (c) => send(F.U_DATA, id, c))
      sock.on('close', () => { send(F.U_CLOSE, id); streams.delete(id) })
      sock.on('error', () => {})
    } else if (type === F.U_DATA) {
      const st = streams.get(id); if (st) try { st.socket.write(payload) } catch {}
    } else if (type === F.U_CLOSE) {
      const st = streams.get(id); if (st) { try { st.socket.end() } catch {} ; streams.delete(id) }
    }
    // PAUSE/RESUME：本参照实现从简（不主动限速）
  })
  ws.on('error', (e) => bad('tunnel client ws', e.message))

  function send(type, id, payload) { if (ws.readyState === 1) ws.send(frame(type, id, payload), { binary: true }) }

  // 读取本地 HTTP 响应：解析响应头 → RES_HEAD；按 content-length / chunked / close 定界转发 body
  function onLocalResp(id, st, chunk) {
    if (st.phase === 'done') return
    if (st.phase === 'head') {
      st.buf = Buffer.concat([st.buf, chunk])
      const idx = st.buf.indexOf('\r\n\r\n')
      if (idx < 0) return
      const headStr = st.buf.slice(0, idx).toString('latin1')
      const rest = st.buf.slice(idx + 4)
      const lines = headStr.split('\r\n')
      const m = /^HTTP\/\d\.\d (\d{3}) ?(.*)$/.exec(lines[0]) || [null, '502', '']
      const status = parseInt(m[1], 10)
      const statusText = m[2] || ''
      const headers = {}
      let te = '', cl = -1
      for (let i = 1; i < lines.length; i++) {
        const p = lines[i].indexOf(':')
        if (p < 0) continue
        const k = lines[i].slice(0, p).trim()
        const v = lines[i].slice(p + 1).trim()
        const lk = k.toLowerCase()
        if (lk === 'transfer-encoding') { te = v.toLowerCase() }
        if (lk === 'content-length') cl = parseInt(v, 10)
        if (!HOP.has(lk)) headers[k] = v // 去 hop-by-hop（含 transfer-encoding）
      }
      st.mode = te.includes('chunked') ? 'chunked' : cl >= 0 ? 'length' : 'close'
      if (st.mode === 'length') { headers['Content-Length'] = String(cl); st.remaining = cl }
      // chunked / close：不带 content-length，交由中继 res 自动 chunk 回浏览器
      send(F.RES_HEAD, id, Buffer.from(JSON.stringify({ status, statusText, headers })))
      st.phase = 'body'
      st.cbuf = Buffer.alloc(0)
      feedBody(id, st, rest)
    } else {
      feedBody(id, st, chunk)
    }
  }
  function feedBody(id, st, chunk) {
    if (st.mode === 'length') {
      const take = chunk.subarray(0, st.remaining)
      if (take.length) send(F.RES_BODY, id, take)
      st.remaining -= take.length
      if (st.remaining <= 0) finishHttp(id, st)
    } else if (st.mode === 'chunked') {
      st.cbuf = Buffer.concat([st.cbuf, chunk])
      // 简易 chunked 解码
      for (;;) {
        const nl = st.cbuf.indexOf('\r\n')
        if (nl < 0) break
        const size = parseInt(st.cbuf.slice(0, nl).toString('latin1').trim(), 16)
        if (Number.isNaN(size)) { st.cbuf = Buffer.alloc(0); break }
        if (size === 0) { finishHttp(id, st); return }
        if (st.cbuf.length < nl + 2 + size + 2) break
        send(F.RES_BODY, id, st.cbuf.subarray(nl + 2, nl + 2 + size))
        st.cbuf = st.cbuf.subarray(nl + 2 + size + 2)
      }
    } else {
      if (chunk.length) send(F.RES_BODY, id, chunk)
    }
  }
  function finishHttp(id, st) {
    if (!st || st.phase === 'done') return
    st.phase = 'done'
    send(F.RES_END, id)
    streams.delete(id)
  }

  return ws
}

// ---------- 通过中继发一个 HTTP 请求（带 Host 头，模拟经 Caddy 反代到达）----------
function relayGet(hostHeader, path) {
  return new Promise((resolve) => {
    const req = http.request({ host: '127.0.0.1', port: RELAY_PORT, path, method: 'GET', headers: { Host: hostHeader } }, (res) => {
      let body = ''
      res.on('data', (d) => (body += d))
      res.on('end', () => resolve({ status: res.statusCode, body }))
    })
    req.on('error', (e) => resolve({ status: 0, body: 'ERR ' + e.message }))
    req.end()
  })
}

async function main() {
  const targets = startTargets()
  const relay = spawn(process.execPath, ['server.js'], {
    cwd: __dirname,
    env: { ...process.env, PORT: String(RELAY_PORT), BASE_DOMAIN: BASE, TUNNEL_SECRET: SECRET },
    stdio: ['ignore', 'ignore', 'inherit'],
  })
  await new Promise((r) => setTimeout(r, 500)) // 等中继 listen

  const client = await new Promise((resolve) => {
    const ws = startTunnelClient((m) => resolve({ ws, m }))
  })
  if (client.m.type === 'tunnel-registered') {
    const a = client.m.active || []
    if (['demo1', 'demo2', 'echo', 'chunk'].every((s) => a.includes(s))) ok('注册：四个前缀全部激活')
    else bad('注册', 'active=' + JSON.stringify(a) + ' conflicts=' + JSON.stringify(client.m.conflicts))
  }

  // 1) apex healthz
  {
    const r = await relayGet(BASE, '/healthz')
    r.status === 200 && r.body === 'ok' ? ok('apex /healthz') : bad('apex /healthz', JSON.stringify(r))
  }
  // 2) 两个前缀不串流：交替各 20 次
  {
    let crosstalk = false
    for (let i = 0; i < 20; i++) {
      const [r1, r2] = await Promise.all([relayGet('demo1.' + BASE, '/a' + i), relayGet('demo2.' + BASE, '/b' + i)])
      if (!r1.body.startsWith('hello from demo1') || !r2.body.startsWith('hello from demo2')) crosstalk = true
      if (!r1.body.includes('/a' + i) || !r2.body.includes('/b' + i)) crosstalk = true
    }
    crosstalk ? bad('两前缀交替 40 次', '存在串流/路径错乱') : ok('两前缀交替 40 次：无串流')
  }
  // 3) chunked 响应正确重组
  {
    const r = await relayGet('chunk.' + BASE, '/')
    r.status === 200 && r.body === 'part-A;part-B;part-C' ? ok('chunked 响应重组') : bad('chunked 响应', JSON.stringify(r))
  }
  // 4) 离线/未启用前缀 → 502
  {
    const r = await relayGet('nope.' + BASE, '/')
    r.status === 502 ? ok('未知前缀 → 502') : bad('未知前缀 502', JSON.stringify(r))
  }
  // 5) tls-check：loopback Host 才应答；活前缀 200 / 未知 403
  {
    const r1 = await relayGet('127.0.0.1', '/internal/tls-check?domain=demo1.' + BASE)
    const r2 = await relayGet('127.0.0.1', '/internal/tls-check?domain=ghost.' + BASE)
    r1.status === 200 && r1.body === 'ok' ? ok('tls-check 活前缀 → 200') : bad('tls-check 活前缀', JSON.stringify(r1))
    r2.status === 403 ? ok('tls-check 未知前缀 → 403') : bad('tls-check 未知前缀', JSON.stringify(r2))
  }
  // 6) 经子域名访问 /internal/tls-check 不得命中内部端点（应被转发到隧道 → 本地服务回内容）
  {
    const r = await relayGet('demo1.' + BASE, '/internal/tls-check?domain=x')
    r.body.startsWith('hello from demo1') ? ok('子域名 /internal/tls-check 被正常转发（未泄露内部端点）') : bad('内部端点隔离', JSON.stringify(r))
  }
  // 7) WebSocket 升级穿透（模拟 HMR）
  {
    const res = await new Promise((resolve) => {
      const c = new WebSocket(`ws://127.0.0.1:${RELAY_PORT}/`, { headers: { Host: 'echo.' + BASE } })
      const to = setTimeout(() => resolve('timeout'), 3000)
      c.on('open', () => c.send('ping'))
      c.on('message', (m) => { clearTimeout(to); resolve(m.toString()); c.close() })
      c.on('error', (e) => { clearTimeout(to); resolve('err ' + e.message) })
    })
    res === 'echo:ping' ? ok('WebSocket 升级穿透（echo:ping）') : bad('WebSocket 穿透', String(res))
  }

  // 收尾
  try { client.ws.close() } catch {}
  targets.forEach((s) => { try { s.close() } catch {} })
  relay.kill()
  await new Promise((r) => setTimeout(r, 200))
  log(failures === 0 ? '\n全部通过' : `\n有 ${failures} 项失败`)
  process.exit(failures === 0 ? 0 : 1)
}

main()
