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

const read = path => readFile(new URL(`../${path}`, import.meta.url), 'utf8')

const frameEditor = await read('src/views/components/element/FrameElement/index.vue')
const frameBase = await read('src/views/components/element/FrameElement/BaseFrameElement.vue')
const operate = await read('src/views/Editor/Canvas/Operate/index.vue')
const canvasTool = await read('src/views/Editor/CanvasTool/index2.vue')
const canvas = await read('src/views/Editor/Canvas/index.vue')
const toolbars = await Promise.all([
  read('src/components/CollapsibleToolbar/index.vue'),
  read('src/components/CollapsibleToolbar/index2.vue'),
  read('src/components/CollapsibleToolbar/index22.vue'),
])

// 编辑态与只读/学生态都必须把 772 路由到文章预览
assert.match(frameEditor, /ArticleReadingPreview/)
assert.match(frameEditor, /isArticleReadingTool/)
assert.match(frameBase, /ArticleReadingPreview/)
assert.match(frameBase, /isArticleReadingTool/)
assert.match(frameBase, /ARTICLE_READING_TOOL_TYPE/)

// 锁定、重置、滚轮放行
assert.match(operate, /isArticleReadingTool/)
assert.match(canvasTool, /ARTICLE_READING_TOOL_TYPE/)
assert.match(canvasTool, /articleStore\.requestResetPreview/)
assert.match(canvas, /article-reading-preview/)

// 三个工具栏名称映射
for (const toolbar of toolbars) {
  assert.match(toolbar, /ARTICLE_READING_TOOL_TYPE/)
  assert.match(toolbar, /ssArticleReadingTool/)
}

// 编号只能来自命名常量：代码里不得出现 772 字面量（注释中说明编号是允许的）
function stripComments(source) {
  return source
    .replace(/<!--[\s\S]*?-->/g, '')
    .replace(/\/\*[\s\S]*?\*\//g, '')
    .replace(/(^|[^:])\/\/.*$/gm, '$1')
}

const codeFiles = {
  frameEditor, frameBase, operate, canvasTool, canvas,
  toolbar0: toolbars[0], toolbar1: toolbars[1], toolbar2: toolbars[2],
}
for (const [name, source] of Object.entries(codeFiles)) {
  assert.doesNotMatch(
    stripComments(source),
    /\b772\b/,
    `${name} must reference ARTICLE_READING_TOOL_TYPE, not the literal 772`,
  )
}

// 话题讨论与句子跟读的既有编号不得被改写
assert.match(frameBase, /77: 'ssEnglishSpeakingTool'/)
assert.match(frameBase, /81: 'ssH5Page'/)

// 文章分支不得回落到话题讨论组件
const articleBranch = frameEditor.slice(frameEditor.indexOf('ArticleReadingPreview'))
assert.doesNotMatch(articleBranch.slice(0, 200), /TopicDiscussionPreview/)

// 老师端 Student runtime 按工具类型分流
const studentView = await read('src/views/Student/index.vue')
const articleClassComposable = await read(
  'src/views/Student/components/ArticleReadingClassPanel/useArticleClassSummary.ts',
)

assert.match(studentView, /ArticleReadingClassPanel/)
assert.match(studentView, /ARTICLE_READING_TOOL_TYPE/)
assert.match(studentView, /TOPIC_DISCUSSION_TOOL_TYPE/)
assert.match(articleClassComposable, /listArticleSessionsByConfig/)
assert.match(articleClassComposable, /generateArticleClassSummary/)

// 作业记录必须带真实工具号，不能写死 77
assert.match(studentView, /atool: String\(activityToolType\)/)
assert.doesNotMatch(studentView, /atool: '77'/)

// 通用结果弹窗/提交按钮对文章与话题一视同仁地抑制
assert.doesNotMatch(studentView, /currentSlideToolType !== 77/)
assert.match(studentView, /articleReadingPanelRef\.value\?\.scheduleRefetch/)

// 三个语言包都要有工具名与班级面板文案
for (const loc of ['cn', 'en', 'hk']) {
  const pack = JSON.parse(await read(`src/views/lang/${loc}.json`))
  for (const key of [
    'ssArticleReadingTool', 'ssArticleReading', 'ssArticleClassSummary',
    'ssArticleFilterCompleted', 'ssArticleFilterReading',
  ]) {
    assert.ok(pack[key], `${loc}.json is missing ${key}`)
  }
}

// ---- 编辑态老师预览不进统计（spec §4.5）：三道门都要在 ArticleReadingPreview 里 ----
const articlePreview = await read('src/views/Editor/EnglishSpeaking/preview/ArticleReadingPreview.vue')

// 门 1：runtime 判定不能只看 userid —— 编辑器 URL 里也有 userid。
// 身份判据用项目既有的 type（Student/index.vue:625：老师端 1 / 学生端 2）。
assert.match(articlePreview, /isStudentRuntime/)
assert.match(articlePreview, /params\.get\('type'\)/)
assert.match(articlePreview, /role === '2'/)

// 门 2：编辑态把 null 传给引擎，不是空串
assert.match(articlePreview, /sessionUserId/)

// 门 3：resume 探针与进度广播只在学生态跑
assert.match(articlePreview, /if \(!isStudentRuntime\.value\) return/)

// 反向：不得再出现「只要有 userid 就当学生」的旧判据
assert.doesNotMatch(articlePreview, /startDisabled = computed\(\(\) => !hasArticle\.value \|\| !runtimeUserId\.value\)/)

// 反向：不得拿 mode 当身份 —— mode=student 是视图，老师上课时也在这个视图里，
// 用它会把上课页的老师当成学生（编辑态那个 bug 换个地方复发），还得再去和
// App.vue 的 localStorage viewMode 对账。type 是身份，与渲染哪个视图无关。
assert.doesNotMatch(articlePreview, /'student'/)
assert.doesNotMatch(articlePreview, /currentViewMode/)
assert.doesNotMatch(await read('src/App.vue'), /currentViewMode/)

// ---- 报告的词级形状必须跟住后端契约（spec §6）：段落下面是**扁平的 words**。
// 后端 c62d35a「flatten scoring/report to paragraph→words」把句层删了,前端的类型和
// 取值路径没跟着改,于是任何一张已完成的段落卡片一展开就 `paragraph.sentences` 是
// undefined、`.flatMap` 抛异常。类型说了谎,所以 type-check 也发现不了 —— 只能在这里钉。
const articleTypes = await read('src/types/articleReading.ts')
assert.match(articleTypes, /words: ArticleWordResult\[\]/)
assert.doesNotMatch(articleTypes, /sentences/)
assert.doesNotMatch(articleTypes, /ArticleSentenceResult/)

const paragraphCard = await read('src/views/Editor/EnglishSpeaking/preview/ArticleParagraphCard.vue')
assert.match(paragraphCard, /props\.paragraph\.words/)
assert.doesNotMatch(paragraphCard, /flattenParagraphWords/)
assert.doesNotMatch(
  await read('src/views/Editor/EnglishSpeaking/preview/articleReadingModel.ts'),
  /sentences/,
)

console.log('article reading routing: all assertions passed')
