/**
 * 老师端班级总结的自动刷新（spec §8.2）
 *
 * 覆盖：班级状态指纹的口径；定时 tick 的四道闸门（重入 / 分页隐藏 / 无人完成 /
 * 指纹未变）；失败不记指纹于是下次 tick 重试；手动刷新同样更新指纹；计时器
 * 间隔与卸载清理。手法承 test-article-auto-format-flow.mjs —— 转译 composable，
 * 把 vue 与 service 换成桩。
 */
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

let source = await read(
  'src/views/Student/components/ArticleReadingClassPanel/useArticleClassSummary.ts',
)
source = source
  .replace(
    "import { ref, watch, onMounted, onUnmounted, type Ref } from 'vue'",
    [
      'const ref = value => ({ value })',
      // watch 的 immediate 由测试自己触发（__watch()），好精确控制每一次 tick
      'const watch = (_src, cb) => { globalThis.__watch = cb }',
      'const onMounted = fn => { globalThis.__mounted = fn }',
      'const onUnmounted = fn => { globalThis.__unmounted = fn }',
    ].join('; '),
  )
  .replace(
    /import \{\s*listArticleSessionsByConfig,\s*generateArticleClassSummary,\s*\} from '@\/views\/Editor\/EnglishSpeaking\/services\/articleReading'/,
    [
      'const listArticleSessionsByConfig = (...a) => globalThis.__list(...a)',
      'const generateArticleClassSummary = (...a) => globalThis.__summary(...a)',
    ].join('; '),
  )

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

const flush = () => new Promise(resolve => setTimeout(resolve, 0))
const row = (userId, status, overallScore = null) => ({
  userId, name: userId, status, overallScore, dims: null, sessionId: 's-' + userId, completedAt: null,
})

/** 组件挂载：驱动 watch 的 immediate 那一次，并跑 onMounted */
function setup({ rows = [], summary } = {}) {
  globalThis.document = { hidden: false, addEventListener() {}, removeEventListener() {} }
  globalThis.__summaryCalls = 0
  globalThis.__list = async () => ({ summaries: current.rows })
  globalThis.__summary = async () => {
    globalThis.__summaryCalls += 1
    if (summary) return summary()
    return { summary: 'text-' + globalThis.__summaryCalls, generatedAt: 'now', fromCache: false, llmStatus: 'ok' }
  }
  const current = { rows }
  const api = useArticleClassSummary({
    configId: { value: 'cfg-1' },
    studentArray: { value: [{ userid: 'u1', name: 'u1' }, { userid: 'u2', name: 'u2' }] },
  })
  globalThis.__mounted()
  return { api, current, tick: async () => { globalThis.__watch(); await flush(); await flush() } }
}

function teardown() {
  globalThis.__unmounted()
}

// ---------------- 指纹口径 ----------------
{
  const a = [row('u1', 'completed', 80), row('u2', 'reading')]
  const b = [row('u2', 'reading'), row('u1', 'completed', 80)]
  assert.equal(classStateKey('c', a), classStateKey('c', b), '与数组顺序无关')

  assert.notEqual(
    classStateKey('c', [row('u1', 'reading')]),
    classStateKey('c', [row('u1', 'completed')]),
    '状态变化要进指纹',
  )
  assert.notEqual(
    classStateKey('c', [row('u1', 'completed', 80)]),
    classStateKey('c', [row('u1', 'completed', 81)]),
    '分数变化要进指纹（分数晚于 status 落地）',
  )
  assert.notEqual(
    classStateKey('c', [row('u1', 'completed', null)]),
    classStateKey('c', [row('u1', 'completed', 0)]),
    'null 与 0 不可混同',
  )
  assert.notEqual(
    classStateKey('cfg-1', a),
    classStateKey('cfg-2', a),
    'configId 进指纹 —— 换配置自动失效',
  )
}

// ---------------- 闸门：全班都没开始 ----------------
{
  const { current, tick } = setup({ rows: [row('u1', 'not_started'), row('u2', 'not_started')] })
  await tick()
  assert.equal(globalThis.__summaryCalls, 0, '一人都没开始 → 不生成（只能写「未开始 N 人」）')

  current.rows = [row('u1', 'reading'), row('u2', 'not_started')]
  await tick()
  assert.equal(globalThis.__summaryCalls, 1, '有人开始朗读就生成，不必等到有人完成')

  current.rows = [row('u1', 'completed', 70), row('u2', 'not_started')]
  await tick()
  assert.equal(globalThis.__summaryCalls, 2, '同一人从进行中变完成 → 再刷新一次')
  teardown()
}

// ---------------- 闸门：指纹未变 ----------------
{
  const { api, current, tick } = setup({ rows: [row('u1', 'completed', 70)] })
  await tick()
  assert.equal(globalThis.__summaryCalls, 1)
  assert.equal(api.aiSummary.value, 'text-1')

  await tick()
  await tick()
  assert.equal(globalThis.__summaryCalls, 1, '班级状态没变，定时 tick 不烧 LLM')
  assert.equal(api.aiSummary.value, 'text-1', '措辞不会无故换掉')

  current.rows = [row('u1', 'completed', 70), row('u2', 'completed', 90)]
  await tick()
  assert.equal(globalThis.__summaryCalls, 2, '有新完成者就刷新')
  teardown()
}

// 分数晚于 status 落地：同一批人、只有分数补上，也算新东西
{
  const { current, tick } = setup({ rows: [row('u1', 'completed', null)] })
  await tick()
  assert.equal(globalThis.__summaryCalls, 1)
  current.rows = [row('u1', 'completed', 76)]
  await tick()
  assert.equal(globalThis.__summaryCalls, 2, '分数回填后重算')
  teardown()
}

// ---------------- 失败不记指纹 → 下次 tick 自动重试 ----------------
{
  let attempt = 0
  const { api, tick } = setup({
    rows: [row('u1', 'completed', 70)],
    summary: () => {
      attempt += 1
      if (attempt === 1) throw new Error('boom')
      return { summary: 'recovered', generatedAt: 'now', fromCache: false, llmStatus: 'ok' }
    },
  })
  const originalError = console.error
  console.error = () => {}
  try {
    await tick()
    assert.equal(api.aiError.value, 'SUMMARY_FAILED')
    assert.equal(api.aiSummary.value, '', '从未成功 → 正文仍为空，由弹框显示错误块')
    await tick()
  }
  finally {
    console.error = originalError
  }
  assert.equal(globalThis.__summaryCalls, 2, '失败不记指纹，下次 tick 重试')
  assert.equal(api.aiSummary.value, 'recovered')
  assert.equal(api.aiError.value, null)
  teardown()
}

// ---------------- 手动刷新也更新指纹 ----------------
{
  const { api, tick } = setup({ rows: [row('u1', 'completed', 70)] })
  await api.fetchClassSummary()
  await api.refreshAISummary()
  assert.equal(globalThis.__summaryCalls, 1, '手动点按钮照常生成')
  await tick()
  assert.equal(globalThis.__summaryCalls, 1, '紧接着的 tick 是 no-op，不必重置计时器')
  teardown()
}

// ---------------- 分页隐藏时跳过，切回来立刻补 ----------------
{
  const listeners = {}
  globalThis.document = {
    hidden: true,
    addEventListener: (type, fn) => { listeners[type] = fn },
    removeEventListener: (type) => { delete listeners[type] },
  }
  globalThis.__summaryCalls = 0
  globalThis.__list = async () => ({ summaries: [row('u1', 'completed', 70)] })
  globalThis.__summary = async () => {
    globalThis.__summaryCalls += 1
    return { summary: 'x', generatedAt: 'now', fromCache: false, llmStatus: 'ok' }
  }
  useArticleClassSummary({
    configId: { value: 'cfg-1' },
    studentArray: { value: [{ userid: 'u1', name: 'u1' }] },
  })
  globalThis.__mounted()
  globalThis.__watch()
  await flush(); await flush()
  assert.equal(globalThis.__summaryCalls, 0, '老师切走的分页不烧 LLM')

  globalThis.document.hidden = false
  listeners.visibilitychange()
  await flush(); await flush()
  assert.equal(globalThis.__summaryCalls, 1, '切回来立刻补一次，不必再等 60 秒')

  globalThis.__unmounted()
  assert.equal(listeners.visibilitychange, undefined, '卸载摘掉监听')
}

// ---------------- 计时器：间隔与清理 ----------------
{
  const originalSet = globalThis.setInterval
  const originalClear = globalThis.clearInterval
  const created = []
  const cleared = []
  globalThis.setInterval = (_fn, ms) => { created.push(ms); return { id: created.length } }
  globalThis.clearInterval = handle => { cleared.push(handle) }
  try {
    const { api } = setup({ rows: [] })
    assert.deepEqual(created, [60_000], '定时间隔 60 秒')
    assert.ok(api.scheduleRefetch, 'WS 去抖刷新仍然对外暴露')
    teardown()
    assert.deepEqual(cleared, [{ id: 1 }], '卸载清掉定时器')
  }
  finally {
    globalThis.setInterval = originalSet
    globalThis.clearInterval = originalClear
  }
}

console.log('✓ test-article-class-summary-auto: all assertions passed')
