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

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

source = source
  .replace(
    "import { ref, reactive, computed, onUnmounted } from 'vue'",
    `
const ref = value => ({ value })
const reactive = value => value
const computed = getter => ({ get value() { return getter() } })
const onUnmounted = () => {}
`,
  )
  .replace(
    "import { createDialogueApi, DialogueApiError } from '../services/llmService'",
    `
const createDialogueApi = () => globalThis.__dialogueApi
class DialogueApiError extends Error {
  constructor(message, status) {
    super(message)
    this.status = status
  }
}
`,
  )
  .replace(
    "import { buildSpeakingWsUrl } from '../services/speakingApiConfig'",
    "const buildSpeakingWsUrl = () => 'wss://example.test/api/speaking/dialogue/speak-stream'",
  )

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 speakCalls = []
globalThis.__dialogueApi = {
  async *speak(sessionId, audioBlob, signal, turnId) {
    speakCalls.push({ sessionId, audioBlob, signal, turnId })
    yield { type: 'transcript', text: 'hello' }
    yield { type: 'token', text: 'Hi.' }
    yield { type: 'done', isComplete: false }
  },
  generateGreeting() {
    throw new Error('not used')
  },
  getReport() {
    throw new Error('not used')
  },
  completeSession() {
    throw new Error('not used')
  },
}

const mod = await import(`data:text/javascript,${encodeURIComponent(compiled)}`)
const engine = mod.useDialogueEngine()
engine.attachSession({ sessionId: 'session-1', totalRounds: 3 })

const ctl = engine.beginStudentStream({ sampleRate: 16000, bits: 16, channels: 1 })
assert.ok(ctl)

const ws = FakeWebSocket.instances[0]
ws.readyState = FakeWebSocket.CLOSED
ws.onerror?.({})
ws.onclose?.({})

const audioBlob = new Blob([new Uint8Array([1, 2, 3, 4])], { type: 'audio/webm' })
ctl.commit(audioBlob)
await new Promise(resolve => setTimeout(resolve, 0))

// WS 失败时不再自动降级 HTTP——api.speak 不应被自动调用。
assert.equal(speakCalls.length, 0, 'auto fallback to /speak should not happen')

// 应当只 push 一条 student 消息（已 splice 掉 ai 占位），处于 error 状态等待用户点重试。
assert.equal(engine.messages.value.length, 1)
const studentMsg = engine.messages.value[0]
assert.equal(studentMsg.role, 'student')
assert.equal(studentMsg.status, 'error')
assert.equal(studentMsg.recovery, 'retry')
assert.equal(studentMsg.audioBlob, audioBlob, 'audioBlob preserved for retry')
assert.equal(studentMsg.turnId, ctl.turnId)

// 用户点重试 → engine.retryMessage 走 sendStudentMessage → /speak（HTTP）。
await engine.retryMessage(studentMsg.id)
assert.equal(speakCalls.length, 1, 'retry should invoke /speak once')
assert.equal(speakCalls[0].sessionId, 'session-1')
assert.equal(speakCalls[0].turnId, ctl.turnId, 'retry reuses original turnId for idempotency')
assert.equal(speakCalls[0].audioBlob, audioBlob)
