import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import ts from 'typescript'

const sourceUrl = new URL('../src/views/Editor/EnglishSpeaking/services/articleReadingStream.ts', import.meta.url)
let source = await readFile(sourceUrl, 'utf8')

source = source.replace(
  "import { buildArticleWsUrl } from './speakingApiConfig'",
  "const buildArticleWsUrl = path => `wss://example.test/api/speaking/article${path}`",
)

const compiled = ts.transpileModule(source, {
  compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 },
}).outputText

class FakeWebSocket {
  static CONNECTING = 0
  static OPEN = 1
  static CLOSING = 2
  static CLOSED = 3
  static instances = []

  readyState = FakeWebSocket.CONNECTING
  binaryType = ''
  sent = []
  onopen = null
  onmessage = null
  onerror = null
  onclose = null

  constructor(url) {
    this.url = url
    FakeWebSocket.instances.push(this)
  }

  send(payload) {
    this.sent.push(payload)
  }

  close() {
    this.readyState = FakeWebSocket.CLOSED
    this.onclose?.({})
  }
}

globalThis.WebSocket = FakeWebSocket

const mod = await import(`data:text/javascript,${encodeURIComponent(compiled)}`)

// ---- 1. 正常时序：start → PCM → stop → ack ----
// chunk 先于 open 抵达，必须排队而不是丢弃（录音器早于 socket 就绪是常态）
const stream = mod.openArticleParagraphStream({
  sessionId: 's1',
  paragraphIdx: 2,
  sampleRate: 48000,
})
const firstChunk = new Uint8Array([1, 2, 3, 4]).buffer
stream.pushChunk(firstChunk)
const ackPromise = stream.stop()

const ws = FakeWebSocket.instances[0]
assert.equal(ws.url, 'wss://example.test/api/speaking/article/session/s1/paragraph/2/stream')
ws.readyState = FakeWebSocket.OPEN
ws.onopen()

assert.deepEqual(JSON.parse(ws.sent[0]), {
  type: 'start', sampleRate: 48000, bits: 16, channels: 1,
})
assert.equal(ws.sent[1], firstChunk)
assert.deepEqual(JSON.parse(ws.sent[2]), { type: 'stop' })

ws.onmessage({ data: JSON.stringify({ type: 'ack', status: 'generating', attempt: 1, truncated: false }) })
assert.deepEqual(await ackPromise, {
  type: 'ack', status: 'generating', attempt: 1, truncated: false,
})

// ---- 2. error frame 以其 message reject ----
const errStream = mod.openArticleParagraphStream({ sessionId: 's2', paragraphIdx: 0, sampleRate: 16000 })
const errAck = errStream.stop()
const errWs = FakeWebSocket.instances[1]
errWs.readyState = FakeWebSocket.OPEN
errWs.onopen()
errWs.onmessage({ data: JSON.stringify({ type: 'error', message: 'assessment failed' }) })
await assert.rejects(errAck, /assessment failed/)

// ---- 3. abort() 关闭连接并 reject 未决的 stop ----
const abortStream = mod.openArticleParagraphStream({ sessionId: 's3', paragraphIdx: 1, sampleRate: 16000 })
const abortWs = FakeWebSocket.instances[2]
abortWs.readyState = FakeWebSocket.OPEN
abortWs.onopen()
const abortAck = abortStream.stop()
abortStream.abort('user left the paragraph')
assert.equal(abortWs.readyState, FakeWebSocket.CLOSED)
await assert.rejects(abortAck, /user left the paragraph/)

// ---- 4. 连接在 ack 之前关闭必须 reject，不能静默挂起 ----
const deadStream = mod.openArticleParagraphStream({ sessionId: 's4', paragraphIdx: 0, sampleRate: 16000 })
const deadWs = FakeWebSocket.instances[3]
deadWs.readyState = FakeWebSocket.OPEN
deadWs.onopen()
const deadAck = deadStream.stop()
deadWs.close()
await assert.rejects(deadAck, /closed before acknowledgement/)

// ---- 5. stop 之后再推 PCM 是编程错误 ----
const lateStream = mod.openArticleParagraphStream({ sessionId: 's5', paragraphIdx: 0, sampleRate: 16000 })
const lateWs = FakeWebSocket.instances[4]
lateWs.readyState = FakeWebSocket.OPEN
lateWs.onopen()
const lateAck = lateStream.stop()
assert.throws(() => lateStream.pushChunk(new Uint8Array([9]).buffer), /Cannot push PCM after stop/)
lateWs.onmessage({ data: JSON.stringify({ type: 'ack', status: 'generating', attempt: 1, truncated: false }) })
await lateAck
