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

const root = new URL('../', import.meta.url)
const read = p => readFile(new URL(p, root), 'utf8')
const transpile = source => ts.transpileModule(source, {
  compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 },
}).outputText

const base = 'src/views/Editor/EnglishSpeaking/composables/'

// 纯规则模块先转译成 data URL，供 composable 原样 import（相对路径在 data: 里无法解析）
const rulesUrl = `data:text/javascript,${encodeURIComponent(
  transpile(await read(`${base}articleAutoFormatRules.ts`)),
)}`

let source = await read(`${base}useArticleAutoFormat.ts`)
source = source
  .replace(
    "import { onScopeDispose, ref, type Ref } from 'vue'",
    // onScopeDispose 桩：把清理回调存到 globalThis，供测试手动模拟"组件已卸载"
    'const ref = value => ({ value }); const onScopeDispose = fn => { globalThis.__onDispose = fn }',
  )
  .replace(
    "import { lang } from '@/main'",
    "const lang = { ssArticleSegmentFailed: 'segment-failed' }",
  )
  .replace(
    "import message from '@/utils/message'",
    'const message = { error: text => globalThis.__errors.push(text) }',
  )
  .replace(
    "import { segmentArticle } from '../services/articleReading'",
    'const segmentArticle = (...args) => globalThis.__segment(...args)',
  )
  // 双引号做外层定界符：rulesUrl 是 encodeURIComponent 输出，单引号/反引号在其字符集里不转义，
  // 若仍用单引号包裹会被规则文件注释里的反引号／`.join(' ')` 里的单引号提前截断字符串。
  .replace("from './articleAutoFormatRules'", `from "${rulesUrl}"`)

const { useArticleAutoFormat } = await import(
  `data:text/javascript,${encodeURIComponent(transpile(source))}`
)

const words = n => Array.from({ length: n }, (_, i) => `w${i}`).join(' ')

function setup({ segment } = {}) {
  globalThis.__errors = []
  globalThis.__segment = segment ?? (async () => ({ content: 'P1.\n\nP2.' }))
  const content = { value: '' }
  return { content, api: useArticleAutoFormat(content), errors: globalThis.__errors }
}

// 失败用例是故意的，composable 会如实 console.error。但它的栈里带着整个 data: URL
// （被转译进去的源码），一条就是好几 KB，会把真正的测试输出淹掉。捕获掉，让通过时的
// 输出保持干净；断言仍然验证 message.error 收到了什么。
async function withSilencedConsoleError(fn) {
  const original = console.error
  console.error = () => {}
  try {
    await fn()
  }
  finally {
    console.error = original
  }
}

// ---------------- 粘贴触发 ----------------
{
  const { content, api } = setup()
  api.onContentInput(words(60))
  assert.equal(content.value, words(60), '内容恒写入（取消时保留原始粘贴）')
  assert.equal(api.dialogVisible.value, true)
  assert.equal(api.dialogMode.value, 'paste')
}
{
  const { api } = setup()
  api.onContentInput(words(40))
  assert.equal(api.dialogVisible.value, false, '短文不弹')
}
{
  // 已有长文再敲一个字符 —— 绝不能弹（prev 取自 content.value，不是陈旧变量）
  const { content, api } = setup()
  content.value = words(300)
  api.onContentInput(`${words(300)} x`)
  assert.equal(api.dialogVisible.value, false)
}

// ---------------- 粘贴 → 取消 → 内容不变（spec §3.2 step1「取消 → 保留原始粘贴」）----------------
{
  const { content, api } = setup()
  api.onContentInput(words(60))
  assert.equal(api.dialogVisible.value, true)
  api.cancelDialog()
  assert.equal(api.dialogVisible.value, false, '取消关框')
  assert.equal(content.value, words(60), '取消不清空/不改写老师刚粘贴的内容')
}

// ---------------- 按钮触发 ----------------
{
  const { content, api } = setup()
  content.value = words(80)
  api.openOverwrite()
  assert.equal(api.dialogMode.value, 'overwrite')
  assert.equal(api.dialogVisible.value, true)
}
{
  const { api } = setup() // 空文本
  api.openOverwrite()
  assert.equal(api.dialogVisible.value, false, '空文本不开框')
}

// ---------------- 应用配置守卫 ----------------
{
  const { content, api } = setup()
  content.value = 'A one.\nB   two.\n\n\nC three.'
  assert.equal(api.guardBeforeApply(), true, '无超长段 → 放行')
  assert.equal(content.value, 'A one.\n\nB two.\n\nC three.', '归一化回写 textarea')
  assert.equal(api.dialogVisible.value, false)
}
{
  // 拦截路径也要归一化回写（脏空白不能留在 textarea）——通行路径已断言，这里补上拦截路径
  const { content, api } = setup()
  const messyOverlong = words(301).replace(/ /g, '   ') // 词间三倍空格
  content.value = `A   one.\n\n\n\n${messyOverlong}`
  assert.equal(api.guardBeforeApply(), false, '超长段 → 拦截')
  assert.equal(content.value, `A one.\n\n${words(301)}`, '拦截也归一化回写')
  assert.equal(api.dialogMode.value, 'guard')
  assert.equal(api.dialogVisible.value, true)
}
{
  // 请求进行中点「应用配置」：guardBeforeApply 必须早退，程序化调用也绕不过去（不止按钮 disabled）
  let release
  const pending = new Promise(resolve => { release = resolve })
  const { content, api } = setup({
    segment: async () => { await pending; return { content: 'Done.' } },
  })
  content.value = words(60)
  api.openOverwrite()
  const confirming = api.confirmDialog() // isFormatting → true
  assert.equal(api.guardBeforeApply(), false, '格式化进行中，guardBeforeApply 必须早退')
  assert.equal(content.value, words(60), '早退不应触碰内容（不归一化、不误判超长）')
  assert.equal(api.dialogMode.value, 'overwrite', 'dialogMode 不应被早退的 guardBeforeApply 改成 guard')
  release()
  await confirming
}

// ---------------- 确认键：文本清空后点确认 ----------------
{
  // 可达路径（非 UI-unreachable 早退）：老师清空文本框再点确认，弹框不能卡住
  const { content, api } = setup()
  content.value = words(60)
  api.openOverwrite()
  content.value = '   '
  await api.confirmDialog()
  assert.equal(api.dialogVisible.value, false, '空文本点确认 —— 关框而非静默无反应')
  assert.equal(api.isFormatting.value, false)
}

// ---------------- 分段成功 ----------------
{
  const calls = []
  const { content, api } = setup({
    segment: async text => { calls.push(text); return { content: 'One.\n\n\nTwo.' } },
  })
  content.value = words(60)
  api.openOverwrite()
  await api.confirmDialog()
  assert.deepEqual(calls, [words(60)], '整篇送后端')
  assert.equal(content.value, 'One.\n\nTwo.', '回填并归一化')
  assert.equal(api.dialogVisible.value, false)
  assert.equal(api.isFormatting.value, false)
}

// ---------------- 分段失败：非守卫框关闭、守卫框保持打开 ----------------
{
  const { content, api, errors } = setup({ segment: async () => { throw new Error('down') } })
  content.value = words(60)
  api.openOverwrite()
  await withSilencedConsoleError(() => api.confirmDialog())
  assert.equal(content.value, words(60), '失败保留原文本')
  assert.equal(api.dialogVisible.value, false)
  assert.deepEqual(errors, ['segment-failed'])
}
{
  const { content, api } = setup({ segment: async () => { throw new Error('down') } })
  content.value = words(400)
  api.guardBeforeApply()
  await withSilencedConsoleError(() => api.confirmDialog())
  assert.equal(api.dialogVisible.value, true, '守卫框失败后保持打开，可原地重试')
  assert.equal(api.isFormatting.value, false)
  assert.equal(content.value, words(400), '守卫框失败同样保留内容（非守卫失败已断言，这里补上守卫路径）')
}

// ---------------- 防重复点击 ----------------
{
  let calls = 0
  let release
  const pending = new Promise(resolve => { release = resolve })
  const { content, api } = setup({
    segment: async () => { calls += 1; await pending; return { content: 'Done.' } },
  })
  content.value = words(60)
  api.openOverwrite()
  const first = api.confirmDialog()
  await api.confirmDialog()          // 进行中再点 —— 必须早退
  assert.equal(calls, 1)
  api.cancelDialog()                  // 进行中取消 —— 必须无效
  assert.equal(api.dialogVisible.value, true)
  release()
  await first
  assert.equal(calls, 1)
  assert.equal(api.dialogVisible.value, false)
}

// ---------------- 陈旧性守卫：等待期间内容被改，响应作废不回写 ----------------
{
  let release
  const pending = new Promise(resolve => { release = resolve })
  const { content, api } = setup({
    segment: async () => { await pending; return { content: 'Stale.' } },
  })
  content.value = words(60)
  api.openOverwrite()
  const confirming = api.confirmDialog()
  content.value = 'teacher typed something newer' // 30 秒等待期间老师又改了文本
  release()
  await confirming
  assert.equal(content.value, 'teacher typed something newer', '内容中途被改 → 响应作废，绝不覆盖')
  assert.equal(api.dialogVisible.value, false, '仍然关框，不让弹框卡住')
}

// ---------------- 组件卸载（effect scope 销毁）后到达的响应是 no-op ----------------
{
  let release
  const pending = new Promise(resolve => { release = resolve })
  const { content, api } = setup({
    segment: async () => { await pending; return { content: 'Late.' } },
  })
  content.value = words(60)
  api.openOverwrite()
  const confirming = api.confirmDialog()
  globalThis.__onDispose() // 模拟宿主组件在响应落地前已卸载
  release()
  await confirming
  assert.equal(content.value, words(60), '销毁后到达的响应不得写 store')
  assert.equal(api.dialogVisible.value, true, '销毁后不再触碰任何本地状态')
}

console.log('article auto-format flow OK')
