import assert from 'node:assert/strict'
import { execFileSync } from 'node:child_process'
import { readdirSync, readFileSync, statSync } from 'node:fs'
import test from 'node:test'
import { fileURLToPath } from 'node:url'

function readSource(path) {
  try {
    return readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
  } catch {
    return ''
  }
}

function readRuleBody(source, selector) {
  const escapedSelector = selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
  const match = source.match(new RegExp(`${escapedSelector}\\s*\\{([\\s\\S]*?)\\}`))
  return match?.[1] || ''
}

function countGifFrameBlocks(buffer) {
  let count = 0
  for (let index = 0; index < buffer.length - 2; index += 1) {
    if (buffer[index] === 0x21 && buffer[index + 1] === 0xf9 && buffer[index + 2] === 0x04) {
      count += 1
    }
  }
  return count
}

function readAssetFrames(assetPath, options = {}) {
  const metadata = execFileSync('identify', ['-format', '%w %h %n\n', assetPath], {
    encoding: 'utf8'
  }).trim().split('\n')[0].split(/\s+/).map(Number)
  const sourceWidth = metadata[0]
  const sourceHeight = metadata[1]
  const frameCount = metadata[2] || 1
  const width = options.width || sourceWidth
  const height = options.height || sourceHeight
  const convertArgs = [assetPath, '-coalesce']
  if (options.width || options.height) {
    convertArgs.push('-resize', `${width}x${height}!`)
  }
  convertArgs.push('-alpha', 'off', '-depth', '8', 'rgb:-')
  const pixels = execFileSync('convert', convertArgs, {
    maxBuffer: 64 * 1024 * 1024
  })
  const frameSize = width * height * 3
  return {
    frameCount: Math.min(frameCount, Math.floor(pixels.length / frameSize)),
    frameSize,
    height,
    pixels,
    width
  }
}

function measureGifMotion(assetPath) {
  const { frameCount, frameSize, pixels } = readAssetFrames(assetPath, { width: 64, height: 64 })
  const delta = (leftFrame, rightFrame) => {
    const leftOffset = leftFrame * frameSize
    const rightOffset = rightFrame * frameSize
    let total = 0
    for (let index = 0; index < frameSize; index += 1) {
      total += Math.abs(pixels[leftOffset + index] - pixels[rightOffset + index])
    }
    return total / frameSize
  }
  const adjacent = Array.from(
    { length: Math.max(0, frameCount - 1) },
    (_, index) => delta(index, index + 1)
  ).sort((left, right) => left - right)
  return {
    medianAdjacentDelta: adjacent[Math.floor(adjacent.length / 2)] || 0,
    seamDelta: delta(frameCount - 1, 0)
  }
}

function measureGifDuration(assetPath) {
  const delays = execFileSync('identify', ['-format', '%T\n', assetPath], {
    encoding: 'utf8'
  }).trim().split('\n').map(Number)
  return delays.reduce((total, delay) => total + delay * 10, 0)
}

function measureOrbAssetPresentation(assetPath) {
  const { frameCount, frameSize, height, pixels, width } = readAssetFrames(assetPath)
  let minimumCornerLuma = 255
  let maximumCornerLuma = 0
  let minimumBackgroundSimilarityRatio = 1
  let minimumForegroundWidthRatio = 1
  let minimumForegroundHeightRatio = 1

  for (let frame = 0; frame < frameCount; frame += 1) {
    const frameOffset = frame * frameSize
    const pixelAt = (x, y) => {
      const offset = frameOffset + (y * width + x) * 3
      return [pixels[offset], pixels[offset + 1], pixels[offset + 2]]
    }
    const corners = [
      pixelAt(0, 0),
      pixelAt(width - 1, 0),
      pixelAt(0, height - 1),
      pixelAt(width - 1, height - 1)
    ]
    const cornerLumas = corners.map((pixel) => (pixel[0] + pixel[1] + pixel[2]) / 3)
    minimumCornerLuma = Math.min(minimumCornerLuma, ...cornerLumas)
    maximumCornerLuma = Math.max(maximumCornerLuma, ...cornerLumas)
    const background = [0, 1, 2].map((channel) => Math.round(
      corners.reduce((total, pixel) => total + pixel[channel], 0) / corners.length
    ))
    let backgroundSimilarity = 0
    let minX = width
    let minY = height
    let maxX = -1
    let maxY = -1

    for (let y = 0; y < height; y += 1) {
      for (let x = 0; x < width; x += 1) {
        const pixel = pixelAt(x, y)
        const diff = pixel.reduce(
          (total, value, channel) => total + Math.abs(value - background[channel]),
          0
        )
        if (diff > 22) {
          minX = Math.min(minX, x)
          minY = Math.min(minY, y)
          maxX = Math.max(maxX, x)
          maxY = Math.max(maxY, y)
        }
        if (diff <= 12) {
          backgroundSimilarity += 1
        }
      }
    }
    if (maxX >= minX && maxY >= minY) {
      minimumForegroundWidthRatio = Math.min(minimumForegroundWidthRatio, (maxX - minX + 1) / width)
      minimumForegroundHeightRatio = Math.min(minimumForegroundHeightRatio, (maxY - minY + 1) / height)
    }
    minimumBackgroundSimilarityRatio = Math.min(
      minimumBackgroundSimilarityRatio,
      backgroundSimilarity / (width * height)
    )
  }

  return {
    minimumCornerLuma,
    maximumCornerLuma,
    minimumBackgroundSimilarityRatio,
    minimumForegroundWidthRatio,
    minimumForegroundHeightRatio,
    width,
    height
  }
}

const appShell = readSource('../src/views/AppShellRouteView.vue')
const workbenchView = readSource('../src/views/PersonalWorkbenchView.vue')
const aiMode = readSource('../src/components/business/PersonalWorkbenchAiMode.vue')
const aiModeTemplate = readSource('../src/components/business/PersonalWorkbenchAiMode.template.html')
const aiModeComposer = readSource('../src/components/business/workbench-ai/WorkbenchAiComposer.vue')
const aiModeFileStrip = readSource('../src/components/business/workbench-ai/WorkbenchAiFileStrip.vue')
const aiModeRuntimeDir = fileURLToPath(new URL('../src/composables/workbenchAiMode/', import.meta.url))
const aiModeRuntime = readdirSync(aiModeRuntimeDir)
  .filter((file) => file.endsWith('.js'))
  .sort()
  .map((file) => readFileSync(new URL(`../src/composables/workbenchAiMode/${file}`, import.meta.url), 'utf8'))
  .join('\n')
const aiModeSurface = `${aiMode}\n${aiModeTemplate}\n${aiModeComposer}\n${aiModeFileStrip}\n${aiModeRuntime}`
const aiModeStyles = readSource('../src/assets/styles/components/personal-workbench-ai-mode.css')
const workbenchViewStyles = readSource('../src/assets/styles/views/personal-workbench-view.css')
const appStyles = readSource('../src/assets/styles/app.css')
const aiBackgroundRule = readRuleBody(aiModeStyles, '.workbench-ai-mode::after')
const orbRule = readRuleBody(aiModeStyles, '.workbench-ai-orb')
const orbImageRule = readRuleBody(aiModeStyles, '.workbench-ai-orb__image')
const composerRule = readRuleBody(aiModeStyles, '.workbench-ai-composer')
const composerTextareaRule = readRuleBody(aiModeStyles, '.workbench-ai-composer textarea')
const fileStripRule = readRuleBody(aiModeStyles, '.workbench-ai-file-strip')
const fileCardRule = readRuleBody(aiModeStyles, '.workbench-ai-file-card')
const orbIconAsset = fileURLToPath(
  new URL('../src/assets/workbench-ai-mode-orb-icon.gif', import.meta.url)
)
const orbIconPngAsset = fileURLToPath(
  new URL('../src/assets/workbench-ai-mode-orb-icon.png', import.meta.url)
)
const orbIconBuffer = readFileSync(orbIconAsset)

test('app shell owns the workbench mode and wires it through topbar and content', () => {
  assert.match(appShell, /function resolveDefaultWorkbenchMode\(user\)\s*\{[\s\S]*isPlatformAdminUser\(user\)[\s\S]*'traditional'[\s\S]*'ai'/)
  assert.match(appShell, /const workbenchMode = ref\(resolveDefaultWorkbenchMode\(currentUser\.value\)\)/)
  assert.doesNotMatch(appShell, /const workbenchMode = ref\('traditional'\)/)
  assert.match(appShell, /watch\(\s*\(\) => currentUser\.value,[\s\S]*resolveDefaultWorkbenchMode\(user\)/)
  assert.match(appShell, /function toggleWorkbenchMode\(\)/)
  assert.match(appShell, /const nextMode = workbenchMode\.value === 'ai' \? 'traditional' : 'ai'/)
  assert.match(appShell, /sidebarCollapsedBeforeAiMode\.value = sidebarCollapsed\.value/)
  assert.match(appShell, /workbenchMode\.value = nextMode/)
  assert.match(appShell, /sidebarCollapsed\.value = sidebarCollapsedBeforeAiMode\.value/)
  assert.match(appShell, /<TopBar[\s\S]*:workbench-mode="workbenchMode"[\s\S]*@toggle-workbench-mode="toggleWorkbenchMode"/)
  assert.match(appShell, /<PersonalWorkbenchView[\s\S]*:workbench-mode="workbenchMode"/)
  assert.match(appShell, /const isAiShellMode = computed\(\(\) => workbenchMode\.value === 'ai'\)/)
  assert.match(appShell, /const isWorkbenchAiMode = computed\(\(\) => activeView\.value === 'workbench' && workbenchMode\.value === 'ai'\)/)
  assert.match(appShell, /'workbench-ai-sidebar-active': isAiShellMode/)
  assert.match(appShell, /'workbench-workarea-ai-mode': isWorkbenchAiMode/)
  assert.match(appStyles, /\.workarea\.workbench-workarea\.workbench-workarea-ai-mode\s*\{[\s\S]*padding:\s*0;[\s\S]*background:\s*transparent;/)
})

test('personal workbench view swaps the traditional dashboard with the AI mode screen', () => {
  assert.match(workbenchView, /import PersonalWorkbenchAiMode from '\.\.\/components\/business\/PersonalWorkbenchAiMode\.vue'/)
  assert.match(workbenchView, /<Transition[\s\S]*name="workbench-mode-fade"[\s\S]*mode="out-in"/)
  assert.match(workbenchView, /<PersonalWorkbenchAiMode[\s\S]*v-if="workbenchMode === 'ai'"[\s\S]*key="ai"/)
  assert.match(workbenchView, /:sidebar-command="aiSidebarCommand"/)
  assert.match(workbenchView, /@conversation-change="emit\('ai-conversation-change', \$event\)"/)
  assert.match(workbenchView, /@conversation-history-change="emit\('ai-conversation-history-change', \$event\)"/)
  assert.match(workbenchView, /@open-document="emit\('open-document', \$event\)"/)
  assert.match(workbenchView, /<PersonalWorkbench[\s\S]*v-else[\s\S]*key="traditional"/)
  assert.match(workbenchView, /workbenchMode:\s*\{[\s\S]*type:\s*String,[\s\S]*default:\s*'traditional'/)
  assert.match(workbenchView, /aiSidebarCommand:\s*\{[\s\S]*type:\s*Object/)
  assert.match(workbenchView, /personal-workbench-view\.css/)
  assert.match(workbenchViewStyles, /\.workbench-mode-fade-enter-active,[\s\S]*\.workbench-mode-fade-leave-active\s*\{[\s\S]*transition:/)
  assert.match(workbenchViewStyles, /\.workbench-mode-fade-enter-from,[\s\S]*\.workbench-mode-fade-leave-to\s*\{[\s\S]*opacity:\s*0;[\s\S]*transform:\s*translateY\(10px\) scale\(0\.992\);/)
  assert.match(workbenchViewStyles, /@media \(prefers-reduced-motion:\s*reduce\)/)
})

test('AI mode screen follows the approved reference structure', () => {
  assert.match(aiModeSurface, /personal-workbench-ai-mode\.css/)
  assert.doesNotMatch(aiModeSurface, /workbench-ai-mode-robot-bg\.png/)
  assert.match(aiModeSurface, /workbench-ai-mode-orb-icon\.gif/)
  assert.match(aiModeSurface, /<img[\s\S]*class="workbench-ai-orb__image"/)
  assert.match(aiModeSurface, /小财管家/)
  assert.match(aiModeSurface, /我是您的小财管家/)
  assert.match(aiModeSurface, /今天我能帮您做点什么？/)
  assert.match(aiModeSurface, /费用测算中，请稍等/)
  assert.match(aiModeSurface, /rows="3"/)
  assert.match(aiModeSurface, /workbench-ai-composer-toolbar/)
  assert.match(aiModeSurface, /<article[\s\S]{0,140}v-for="file in runtime\.selectedFileCards"[\s\S]{0,140}class="workbench-ai-file-card"/)
  assert.match(aiModeSurface, /class="workbench-ai-file-card__ocr"/)
  assert.match(aiModeSurface, /file\.ocrState\?\.label/)
  assert.match(aiModeSurface, /mdi mdi-text-recognition/)
  assert.match(aiModeStyles, /\.workbench-ai-file-card__ocr/)
  assert.match(aiModeStyles, /workbenchAiOcrSpin/)
  assert.match(aiModeSurface, /:aria-label="`移除附件 \$\{file\.name\}`"/)
  assert.match(aiModeSurface, /function removeAiModeFile\(fileKey\)/)
  assert.match(aiModeSurface, /const selectedFileCards = computed/)
  assert.match(aiModeSurface, /resolveAiComposerFileType\(file, previewAsset\)/)
  assert.match(aiModeSurface, /AI_COMPOSER_FILE_TYPE_META = \{[\s\S]*pdf:\s*\{ label:\s*'PDF'/)
  assert.match(aiModeSurface, /buildFileIdentity,[\s\S]*collectReceiptFiles[\s\S]*travelReimbursementAttachmentModel\.js/)
  assert.match(aiModeSurface, /MAX_ATTACHMENTS,[\s\S]*mergeFilesWithLimit[\s\S]*travelReimbursementAttachmentModel\.js/)
  assert.match(aiModeSurface, /import \* as aiAttachmentAssociationModel from '\.\.\/\.\.\/utils\/aiAttachmentAssociationModel\.js'/)
  assert.match(aiModeSurface, /aiAttachmentAssociationModel\.buildAiAttachmentAssociationResultMessage/)
  assert.match(aiModeSurface, /syncExpenseClaimFilesToDraft/)
  assert.match(aiModeSurface, /createAttachmentAssociationJob[\s\S]*services\/attachmentAssociationJobs\.js/)
  assert.match(aiModeSurface, /fetchAttachmentAssociationJob[\s\S]*services\/attachmentAssociationJobs\.js/)
  assert.match(aiModeSurface, /import \{ recognizeOcrFiles \} from '\.\.\/\.\.\/services\/ocr\.js'/)
  assert.match(aiModeSurface, /const AI_ATTACHMENT_ASSOCIATION_CONFIRM_ACTION = 'confirm_ai_attachment_association'/)
  assert.match(aiModeSurface, /const AI_ATTACHMENT_OCR_DETAIL_ACTION = 'show_ai_attachment_ocr_details'/)
  assert.match(aiModeSurface, /function isLikelyReceiptAssociationFile\(file = \{\}\)/)
  assert.match(aiModeSurface, /function streamOrSetInlineAssistantContent\(messageId, content\)/)
  assert.match(aiModeSurface, /ai-trusted-html:start/)
  assert.match(aiModeSurface, /class="workbench-ai-ocr-detail-panel"/)
  assert.match(aiModeSurface, /附件识别明细/)
  assert.match(aiModeSurface, /attachmentOcrDetails: normalizeInlineAttachmentOcrDetails/)
  assert.match(aiModeSurface, /function buildInlineAttachmentOcrDetails\(collected = \{\}, files = \[\]\)/)
  assert.match(aiModeSurface, /function toggleInlineAttachmentOcrDetails\(message = \{\}, forceExpanded = null\)/)
  assert.match(aiModeSurface, /function resolveLegacyAiAttachmentAssociationPayload\(content = ''\)/)
  assert.match(aiModeSurface, /function hydrateInlineAttachmentAssociationSuggestedActions\(actions = \[\], content = ''\)/)
  assert.match(aiModeSurface, /label:\s*'确认自动关联'/)
  assert.match(aiModeSurface, /function shouldRunAiAttachmentAutoAssociation\(entry = \{\}, files = \[\], prompt = ''\)/)
  assert.match(aiModeSurface, /files\.every\(\(file\) => isLikelyReceiptAssociationFile\(file\)\)/)
  assert.match(aiModeSurface, /!shouldKeepAiAttachmentInAssistantReply\(prompt\)/)
  assert.match(aiModeSurface, /const aiAttachmentAssociationRuntime = new Map\(\)/)
  assert.match(aiModeSurface, /function findAiAttachmentAssociationRuntime\(options = \{\}\)/)
  assert.match(aiModeSurface, /resolveAiAttachmentAssociationClaimNo\(actionPayload\)/)
  assert.match(aiModeSurface, /if \(actionType === AI_ATTACHMENT_OCR_DETAIL_ACTION\)/)
  assert.match(aiModeSurface, /const collected = await collectAiModeReceiptContext\(files\)/)
  assert.match(aiModeSurface, /function extractReceiptIdsFromOcrDocuments\(documents = \[\]\)/)
  assert.match(aiModeSurface, /const receiptIds = attachmentJobFlow\.extractReceiptIdsFromOcrDocuments\(collected\.ocrDocuments\)/)
  assert.match(aiModeSurface, /await createAttachmentAssociationJob\(\{[\s\S]*receipt_ids: receiptIds,[\s\S]*conversation_id: conversationId\?\.value/)
  assert.match(aiModeSurface, /attachmentAssociationJob: job/)
  assert.match(aiModeSurface, /async function pollJob\(/)
  assert.match(aiModeSurface, /fetchAttachmentAssociationJob\(normalizedJobId\)/)
  assert.match(aiModeSurface, /function resumePendingJobs\(\)/)
  assert.match(aiModeSurface, /resumePendingAiAttachmentAssociationJobs: attachmentJobFlow\.resumePendingJobs/)
  assert.match(aiModeSurface, /attachmentFlow\.resumePendingAiAttachmentAssociationJobs\(\)/)
  assert.match(aiModeSurface, /attachmentAssociationJob: normalizeInlineAttachmentAssociationJob/)
  assert.match(aiModeSurface, /async function confirmAiAttachmentAssociation\(actionPayload = \{\}, sourceMessage = null\)/)
  assert.match(aiModeSurface, /syncExpenseClaimFilesToDraft\(\{[\s\S]*fetchExpenseClaimDetail,[\s\S]*createExpenseClaimItem,[\s\S]*uploadExpenseClaimItemAttachment/)
  assert.match(aiModeSurface, /if \(actionType === AI_ATTACHMENT_ASSOCIATION_CONFIRM_ACTION\)/)
  assert.match(aiModeSurface, /if \(shouldRunAiAttachmentAutoAssociation\(entry, files, cleanPrompt\)\) \{[\s\S]*requestAiAttachmentAssociationReply\(cleanPrompt, entry, files\)/)
  assert.match(aiModeSurface, /const fileMergeResult = mergeFilesWithLimit\(selectedFiles\.value, Array\.from\(event\.target\.files \|\| \[\]\), MAX_ATTACHMENTS\)/)
  assert.match(aiModeSurface, /selectedFiles\.value = fileMergeResult\.files/)
  assert.doesNotMatch(aiModeSurface, /selectedFiles\.value = Array\.from\(event\.target\.files \|\| \[\]\)\.slice\(0, 10\)/)
  assert.doesNotMatch(aiModeSurface, /已选择 \{\{ selectedFiles\.length \}\} 份附件/)
  assert.match(aiModeSurface, /Axiom Ultra 3\.1/)
  assert.match(aiModeSurface, /mdi mdi-calendar-range/)
  assert.match(aiModeSurface, /workbench-ai-date-popover/)
  assert.match(aiModeSurface, /type="date"/)
  assert.match(aiModeSurface, /:min="resolveInlineApplicationPreviewEditorDateMin\(message, row\.key\)"/)
  assert.match(aiModeSurface, /:max="resolveInlineApplicationPreviewEditorDateMax\(message, row\.key\)"/)
  assert.match(aiModeSurface, /resolveInlineApplicationPreviewEditorControl\(row\.key\) === 'date'/)
  assert.match(aiModeSurface, /class="\['application-preview-input', 'application-preview-date-input', `application-preview-input--\$\{row\.key\}`\]"/)
  assert.match(aiModeSurface, /function resolveInlineApplicationPreviewEditorControl\(fieldKey\) \{[\s\S]*return resolveApplicationPreviewEditorControl\(fieldKey\)/)
  assert.match(aiModeSurface, /function resolveInlineApplicationPreviewEditorDateMin\(message, fieldKey\) \{[\s\S]*return resolveApplicationPreviewEditorDateMin\?\.\(message, fieldKey\) \|\| ''/)
  assert.match(aiModeSurface, /function resolveInlineApplicationPreviewEditorDateMax\(message, fieldKey\) \{[\s\S]*return resolveApplicationPreviewEditorDateMax\?\.\(message, fieldKey\) \|\| ''/)
  assert.doesNotMatch(aiModeSurface, /return control === 'date' \? 'text' : control/)
  assert.doesNotMatch(aiModeSurface, /mdi mdi-web/)
  assert.match(aiModeSurface, /mdi mdi-microphone-outline/)
  assert.match(aiModeSurface, /mdi mdi-arrow-up/)
  assert.match(aiModeSurface, /快速开始/)
  assert.match(aiModeSurface, /action-icon-wrapper/)
  assert.match(aiModeSurface, /发起报销/)
  assert.match(aiModeSurface, /查询预算/)
  assert.match(aiModeSurface, /解释制度/)
  assert.match(aiModeSurface, /催办审批/)
  assert.match(aiModeSurface, /<Transition name="workbench-ai-panel-swap" mode="out-in" appear>/)
  assert.match(aiModeSurface, /@submit\.prevent="runtime\.submitAiModePrompt"/)
  assert.equal((aiModeSurface.match(/<WorkbenchAiComposer\b/g) || []).length, 2)
  assert.equal((aiModeSurface.match(/type="submit"[\s\S]{0,160}class="workbench-ai-send-btn"/g) || []).length, 1)
  assert.match(aiModeSurface, /class="workbench-ai-conversation"/)
  assert.match(aiModeSurface, /class="workbench-ai-thread"[\s\S]*@scroll\.passive="handleInlineConversationScroll"/)
  assert.match(aiModeSurface, /workbench-ai-answer-card/)
  assert.match(aiModeSurface, /workbench-ai-answer-markdown/)
  assert.match(aiModeSurface, /v-html="renderInlineConversationHtml\(message\.content\)"/)
  assert.match(aiModeSurface, /workbench-ai-message-actions/)
  assert.match(aiModeSurface, /workbench-ai-conversation-actions/)
  assert.match(aiModeSurface, /scrollInlineConversationToTop/)
  assert.match(aiModeSurface, /requestDeleteCurrentConversation/)
  assert.match(aiModeSurface, /confirmDeleteConversation/)
  assert.match(aiModeSurface, /workbench-ai-confirm-dialog/)
  assert.match(aiModeSurface, /workbench-ai-thinking-toggle/)
  assert.match(aiModeSurface, /小财业务思考/)
  assert.match(aiModeSurface, /class="workbench-ai-thinking-expanded"/)
  assert.match(aiModeSurface, /class="workbench-ai-thinking-collapse-btn"/)
  assert.match(aiModeSurface, /class="workbench-ai-thinking-collapse-btn"[\s\S]*@click="toggleInlineThinking\(message\)"/)
  assert.doesNotMatch(aiModeSurface, /:disabled="message\.pending"/)
  assert.match(aiModeSurface, /isInlineThinkingExpanded/)
  assert.match(aiModeSurface, /toggleInlineThinking/)
  assert.match(aiModeSurface, /const thinkingCollapsedMessageIds = ref\(new Set\(\)\)/)
  assert.match(aiModeSurface, /thinkingCollapsedMessageIds\.value\.has\(message\.id\)/)
  assert.match(aiModeSurface, /nextCollapsedIds\.add\(message\.id\)/)
  assert.match(aiModeSurface, /nextCollapsedIds\.delete\(message\.id\)/)
  assert.match(aiModeSurface, /message\.pending && !hasInlineThinking\(message\)/)
  assert.doesNotMatch(aiModeSurface, /小财管家正在思考/)
  assert.doesNotMatch(aiModeSurface, /思考过程/)
  assert.doesNotMatch(aiModeSurface, /message\.pending \?/)
  assert.match(aiModeSurface, /继续和小财管家对话\.\.\./)
  assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card\)/)
  assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-query-summary\)/)
  assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-query-summary__scope\)/)
  assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card-list\) \{[\s\S]*gap:\s*16px;/)
  assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card\) \{[\s\S]*background-color:\s*#ffffff;/)
  assert.doesNotMatch(aiModeStyles, /ai-document-card-bg\.png/)
  assert.doesNotMatch(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card\)::before/)
  assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__head\) \{[\s\S]*background: var\(--ai-document-card-head-bg\);/)
  assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card\.is-success \.ai-document-card__head\)/)
  assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__head\)/)
  assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__body\)/)
  assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__summary\)/)
  assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__details\)/)
  assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__field\)/)
  assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__field--wide\)/)
  assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__label\)/)
  assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__amount\)/)
  assert.match(
    aiModeStyles,
    /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__details\) \{[\s\S]*grid-template-columns: repeat\(2, minmax\(0, 1fr\)\);/
  )
  assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__field--action \.ai-document-card__action\)/)
  assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-html-action-link\)/)
  assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-html-table-wrap\)/)
  assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-html-image-frame\)/)
  assert.match(aiModeStyles, /\.application-preview-date-input\s*\{[\s\S]*width:\s*min\(100%,\s*188px\);/)
  assert.match(aiModeStyles, /\.application-preview-input--location\s*\{[\s\S]*width:\s*min\(100%,\s*220px\);/)
  assert.match(aiModeStyles, /\.application-preview-input--reason\s*\{[\s\S]*width:\s*min\(100%,\s*680px\);/)
  assert.match(aiModeSurface, /import \{ fetchSettings \} from '\.\.\/\.\.\/services\/settings\.js'/)
  assert.match(aiModeSurface, /fetchStewardPlan,[\s\S]*fetchStewardPlanStream[\s\S]*services\/steward\.js'/)
  assert.match(aiModeSurface, /import \{ useWorkbenchComposerDate \} from '\.\.\/useWorkbenchComposerDate\.js'/)
  assert.match(aiModeSurface, /loadAiWorkbenchConversationHistory/)
  assert.match(aiModeSurface, /saveAiWorkbenchConversation/)
  assert.match(aiModeSurface, /deleteAiWorkbenchConversation/)
  assert.match(aiModeSurface, /import \{ renderAiConversationHtml \} from '\.\.\/\.\.\/utils\/aiConversationHtmlRenderer\.js'/)
  assert.match(aiModeSurface, /function renderInlineConversationHtml\(content\) \{[\s\S]*return renderAiConversationHtml\(content\)[\s\S]*\}/)
  assert.doesNotMatch(aiModeSurface, /import \{ renderMarkdown \} from '\.\.\/\.\.\/utils\/markdown\.js'/)
  assert.match(aiModeSurface, /buildStewardPlanRequest/)
  assert.match(aiModeSurface, /buildStewardPlanMessageText/)
  assert.match(aiModeSurface, /buildStewardSuggestedActions/)
  assert.match(aiModeSurface, /const emit = defineEmits\(\['conversation-change', 'conversation-history-change', 'open-document', 'request-updated'\]\)/)
  assert.match(aiModeSurface, /function startInlineConversation\(prompt, entry = \{\}, files = \[\]\)/)
  assert.match(aiModeSurface, /activateInlineConversation\(\{[\s\S]*title:[\s\S]*\}\)[\s\S]*conversationMessages\.value\.push\(createInlineMessage\('user'/)
  assert.match(aiModeSurface, /persistCurrentConversation\(\)/)
  assert.match(aiModeSurface, /refreshConversationHistory\(\)/)
  assert.match(aiModeSurface, /fetchStewardPlanStream\(/)
  assert.match(aiModeSurface, /fetchStewardPlan\(/)
  assert.match(aiModeSurface, /const INLINE_ANSWER_STREAM_CHUNK_SIZE = 6/)
  assert.match(aiModeSurface, /function updateInlineMessageContent\(message, content\)/)
  assert.match(aiModeSurface, /async function streamInlineAssistantContent\(messageId, content\)/)
  assert.match(aiModeSurface, /const requiredApplicationContinuationFlow = resolveRequiredApplicationGateContinuationFlow\(normalizedPlan\)/)
  assert.match(aiModeSurface, /const finalMessageText = requiredApplicationContinuationFlow[\s\S]*buildAiRequiredApplicationGateAutoMessage\(normalizedPlan, requiredApplicationContinuationFlow\)[\s\S]*buildStewardPlanMessageText\(plan\)/)
  assert.match(aiModeSurface, /const hasServerStreamedContent = Boolean\(String\(pendingMessage\.content \|\| ''\)\.trim\(\)\)/)
  assert.match(aiModeSurface, /if \(!hasServerStreamedContent\) \{[\s\S]*await streamInlineAssistantContent\(pendingMessage\.id, finalMessageText\)[\s\S]*\}/)
  assert.match(aiModeSurface, /if \(actionType === AI_APPLICATION_ACTION_SUBMIT\) \{[\s\S]*buildInlineApplicationResultTable\(draftPayload/)
  assert.match(aiModeSurface, /\| 单据类型 \| 单据编号 \| 单据状态 \| 当前节点 \| 日期 \| 地点 \| 事由 \| 金额 \|/)
  assert.doesNotMatch(aiModeSurface, /需要查看完整详情时，请点击卡片“操作”行的“查看”进入单据详情。/)
  assert.match(aiModeSurface, /function buildInlineApplicationActionFailureText\(error, isSubmit\)/)
  assert.match(aiModeSurface, /我已保留当前申请核对表/)
  assert.match(aiModeSurface, /applicationPreview:\s*targetMessage\.applicationPreview/)
  assert.match(
    aiModeSurface,
    /suggestedActions:\s*buildInlineApplicationPreviewSuggestedActions\([\s\S]*targetMessage\.applicationPreview/
  )
  assert.match(aiModeSurface, /function isOrphanInlineApplicationPreviewMessage\(message = \{\}\)/)
  assert.match(aiModeSurface, /function resolveLatestOrphanApplicationPreviewMessage\(messages = \[\]\)/)
  assert.match(aiModeSurface, /function resolveLatestOrphanInlineApplicationPreviewMessage\(\)/)
  assert.match(aiModeSurface, /当前申请核对表状态不完整，我先重新生成可编辑表格。/)
  assert.match(
    aiModeSurface,
    /const previewSourceText = resolveLatestInlineUserPrompt\(\)[\s\S]*pushInlineApplicationActionUserMessage\(prompt\)[\s\S]*startAiApplicationPreview\('travel', '差旅费', previewSourceText/
  )
  assert.doesNotMatch(aiModeSurface, /\*\*申请单号：\*\*/)
  assert.doesNotMatch(aiModeSurface, /createInlineMessage\('assistant', buildStewardPlanMessageText\(plan\)/)
  assert.doesNotMatch(aiModeSurface, /runOrchestrator\(/)
  assert.doesNotMatch(aiModeSurface, /buildFallbackAnswer/)
  assert.doesNotMatch(aiModeSurface, /已使用本地回复/)
  assert.doesNotMatch(aiModeSurface, /emit\('open-assistant'/)
  assert.match(aiModeStyles, /--ai-theme-rgb:\s*var\(--theme-primary-rgb/)
  assert.match(aiModeStyles, /\.workbench-ai-mode\s*\{[\s\S]*min-height:\s*100%;[\s\S]*background:/)
  assert.match(aiModeStyles, /\.workbench-ai-mode\.has-conversation\s*\{[\s\S]*place-items:\s*stretch;[\s\S]*padding:\s*0;/)
  assert.match(aiModeStyles, /\.workbench-ai-composer\s*\{[\s\S]*border-radius:\s*20px;[\s\S]*box-shadow:/)
  assert.match(fileStripRule, /flex-wrap:\s*nowrap;/)
  assert.match(fileStripRule, /overflow-x:\s*auto;/)
  assert.match(fileStripRule, /justify-content:\s*flex-start;/)
  assert.match(fileCardRule, /grid-template-columns:\s*48px minmax\(0,\s*1fr\) 30px;/)
  assert.match(fileCardRule, /border-radius:\s*16px;/)
  assert.match(aiModeStyles, /\.workbench-ai-file-card__body strong,[\s\S]*\.workbench-ai-file-card__body small\s*\{[\s\S]*text-overflow:\s*ellipsis;/)
  assert.match(aiModeStyles, /\.workbench-ai-file-card__remove\s*\{[\s\S]*border-radius:\s*999px;/)
  assert.match(aiModeStyles, /\.ai-attachment-association-card/)
  assert.match(aiModeStyles, /\.ai-ocr-recognition-card/)
  assert.match(aiModeStyles, /\.ai-attachment-association__note/)
  assert.match(aiModeStyles, /\.workbench-ai-ocr-detail-panel/)
  assert.match(aiModeStyles, /\.workbench-ai-ocr-document__fields/)
  assert.match(composerRule, /min-height:\s*154px;/)
  assert.match(composerRule, /grid-template-rows:\s*minmax\(80px,\s*1fr\) auto;/)
  assert.match(composerTextareaRule, /min-height:\s*80px;/)
  assert.doesNotMatch(aiModeStyles, /--workbench-ai-robot-image/)
  assert.match(aiBackgroundRule, /inset:\s*0;/)
  assert.match(aiBackgroundRule, /linear-gradient\(90deg,\s*rgba\(var\(--ai-theme-rgb\)/)
  assert.match(aiBackgroundRule, /background-size:\s*56px 56px,\s*56px 56px,\s*auto,\s*auto;/)
  assert.match(aiModeStyles, /\.workbench-ai-orb\s*\{[\s\S]*border-radius:\s*50%;/)
  assert.match(orbRule, /rgba\(255,\s*255,\s*255,\s*0\.98\)/)
  assert.match(orbRule, /rgba\(47,\s*124,\s*255,\s*0\.18\)/)
  assert.match(orbRule, /width:\s*clamp\(118px,\s*8vw,\s*132px\);/)
  assert.match(orbRule, /height:\s*clamp\(118px,\s*8vw,\s*132px\);/)
  assert.match(orbRule, /animation:\s*workbenchAiControlIn/)
  assert.match(orbImageRule, /width:\s*100%;/)
  assert.match(orbImageRule, /height:\s*100%;/)
  assert.match(orbImageRule, /object-fit:\s*contain;/)
  assert.match(orbImageRule, /object-position:\s*center center;/)
  assert.doesNotMatch(orbImageRule, /transform:/)
  assert.match(aiModeStyles, /@keyframes workbenchAiControlIn\s*\{[\s\S]*opacity:\s*0;[\s\S]*translateY\(18px\)[\s\S]*opacity:\s*1;/)
  assert.match(aiModeStyles, /\.workbench-ai-copy\s*\{[\s\S]*animation:\s*workbenchAiControlIn/)
  assert.match(aiModeStyles, /\.workbench-ai-composer\s*\{[\s\S]*animation:\s*workbenchAiControlIn/)
  assert.match(aiModeStyles, /\.workbench-ai-composer textarea\s*\{[\s\S]*animation:\s*workbenchAiControlIn/)
  assert.match(aiModeStyles, /\.workbench-ai-icon-btn\s*\{[\s\S]*animation:\s*workbenchAiControlIn/)
  assert.match(aiModeStyles, /\.workbench-ai-send-btn\s*\{[\s\S]*animation:\s*workbenchAiControlIn/)
  assert.match(aiModeStyles, /\.workbench-ai-action:nth-child\(4\)\s*\{[\s\S]*animation-delay:\s*520ms;/)
  assert.match(aiModeStyles, /\.workbench-ai-conversation\s*\{[\s\S]*grid-template-rows:\s*minmax\(0,\s*1fr\) auto;/)
  assert.match(aiModeSurface, /const inlineConversationAutoScrollPinned = ref\(true\)/)
  assert.match(aiModeSurface, /const INLINE_AUTO_SCROLL_THRESHOLD = 96/)
  assert.match(aiModeSurface, /const INLINE_LAYOUT_SETTLE_SCROLL_DELAY_MS = 260/)
  assert.match(aiModeSurface, /function isInlineConversationNearBottom\(\)/)
  assert.match(aiModeSurface, /function handleInlineConversationScroll\(\)\s*\{[\s\S]*inlineConversationAutoScrollPinned\.value = isInlineConversationNearBottom\(\)[\s\S]*\}/)
  assert.match(aiModeSurface, /function forceInlineConversationToBottom\(\)/)
  assert.match(aiModeSurface, /el\.scrollTop = el\.scrollHeight/)
  assert.match(aiModeSurface, /function scrollInlineConversationToBottom\(options = \{\}\)/)
  assert.match(aiModeSurface, /const shouldScroll = options\.force !== false/)
  assert.match(aiModeSurface, /if \(!shouldScroll\) \{[\s\S]*return[\s\S]*\}/)
  assert.match(aiModeSurface, /window\.requestAnimationFrame\(\(\) => \{[\s\S]*forceInlineConversationToBottom\(\)[\s\S]*\}\)/)
  assert.match(aiModeSurface, /window\.setTimeout\(\(\) => \{[\s\S]*if \(inlineConversationAutoScrollPinned\.value\) \{[\s\S]*forceInlineConversationToBottom\(\)[\s\S]*\}[\s\S]*\}, INLINE_LAYOUT_SETTLE_SCROLL_DELAY_MS\)/)
  assert.match(aiModeSurface, /const shouldAutoScroll = inlineConversationAutoScrollPinned\.value[\s\S]*updateInlineMessageContent\(message, streamedContent\)[\s\S]*scrollInlineConversationToBottom\(\{ force: shouldAutoScroll \}\)/)
  assert.match(aiModeSurface, /const shouldAutoScroll = inlineConversationAutoScrollPinned\.value[\s\S]*appendInlineMessageContent\(message, data\.delta \|\| data\.content \|\| data\.text \|\| ''\)[\s\S]*scrollInlineConversationToBottom\(\{ force: shouldAutoScroll \}\)/)
  assert.match(aiModeSurface, /inlineConversationAutoScrollPinned\.value = true[\s\S]*conversationMessages\.value\.push\(createInlineMessage\('user', cleanPrompt\)\)/)
  assert.match(aiModeSurface, /function openInlineRecentConversation\(item = \{\}\) \{[\s\S]*inlineConversationAutoScrollPinned\.value = true[\s\S]*conversationMessages\.value =/)
  assert.doesNotMatch(aiModeSurface, /scrollTo\(\{ top: el\.scrollHeight, behavior: 'smooth' \}\)/)
  assert.match(aiModeStyles, /\.workbench-ai-thread\s*\{[\s\S]*display:\s*flex;[\s\S]*flex-direction:\s*column;[\s\S]*overflow-y:\s*auto;[\s\S]*scrollbar-width:\s*none;/)
  assert.match(aiModeStyles, /\.workbench-ai-thread\s*>\s*:first-child\s*\{[\s\S]*margin-top:\s*auto;/)
  assert.match(aiModeStyles, /\.workbench-ai-message\s*\{[\s\S]*flex:\s*0 0 auto;/)
  assert.match(aiModeStyles, /\.workbench-ai-empty-thread\s*\{[\s\S]*flex:\s*0 0 auto;/)
  assert.doesNotMatch(aiModeStyles, /align-content:\s*end;/)
  assert.doesNotMatch(aiModeStyles, /\.workbench-ai-thread\s*\{[\s\S]*scroll-behavior:\s*smooth;/)
  assert.match(aiModeStyles, /\.workbench-ai-thread::-webkit-scrollbar\s*\{[\s\S]*display:\s*none;/)
  assert.match(aiModeStyles, /\.workbench-ai-conversation-bottom\s*\{[\s\S]*position:\s*relative;[\s\S]*z-index:\s*6;/)
  assert.doesNotMatch(aiModeStyles, /\.workbench-ai-conversation-bottom\s*\{[\s\S]*position:\s*sticky;/)
  assert.doesNotMatch(aiModeStyles, /\.workbench-ai-conversation-bottom\s*\{[\s\S]*bottom:\s*0;/)
  assert.match(aiModeStyles, /\.workbench-ai-conversation-bottom::before\s*\{[\s\S]*display:\s*none;/)
  assert.match(aiModeStyles, /\.workbench-ai-thinking-panel\s*\{[\s\S]*display:\s*grid;[\s\S]*border:\s*1px solid rgba\(191,\s*219,\s*254,\s*0\.58\);/)
  assert.match(aiModeStyles, /\.workbench-ai-thinking-toggle\s*\{[\s\S]*border:\s*0;[\s\S]*background:\s*transparent;/)
  assert.match(aiModeStyles, /\.workbench-ai-thinking-list\s*\{[\s\S]*border:\s*0;[\s\S]*background:\s*transparent;[\s\S]*overflow:\s*visible;/)
  assert.match(aiModeStyles, /\.workbench-ai-thinking-item\s*\{[\s\S]*grid-template-columns:\s*18px minmax\(0,\s*1fr\);/)
  assert.match(aiModeStyles, /\.workbench-ai-thinking-dot\s*\{[\s\S]*justify-self:\s*center;/)
  assert.doesNotMatch(aiModeStyles, /\.workbench-ai-thinking-collapse-btn:disabled/)
  assert.match(aiModeStyles, /\.workbench-ai-thinking-collapse-enter-active,[\s\S]*\.workbench-ai-thinking-collapse-leave-active\s*\{[\s\S]*max-height 220ms ease/)
  assert.match(aiModeStyles, /\.workbench-ai-confirm-dialog\s*\{[\s\S]*border-radius:\s*18px;/)
  assert.match(aiModeStyles, /\.workbench-ai-answer-card\s*\{[\s\S]*box-shadow:\s*none;[\s\S]*backdrop-filter:\s*none;/)
  assert.match(aiModeStyles, /\.workbench-ai-answer-markdown\s*\{[\s\S]*line-height:\s*1\.86;/)
  assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(h3\)\s*\{[\s\S]*font-size:\s*21px;/)
  assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-html-focus-grid\)\s*\{[\s\S]*border-left:\s*3px solid/)
  assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-html-focus-card\)\s*\{[\s\S]*background:\s*transparent;/)
  assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-html-step-index\)\s*\{[\s\S]*background:\s*transparent;[\s\S]*font-size:\s*17px;/)
  assert.match(aiModeStyles, /\.workbench-ai-date-popover\s*\{[\s\S]*animation:\s*workbenchAiPopoverIn/)
  assert.match(aiModeStyles, /\.workbench-ai-send-btn:not\(:disabled\)\s*\{[\s\S]*linear-gradient\(135deg,[\s\S]*#1d4ed8/)
  assert.match(aiModeStyles, /\.workbench-ai-composer--inline\s*\{[\s\S]*min-height:\s*126px;[\s\S]*box-shadow:\s*none;/)
  assert.match(aiModeStyles, /@media \(prefers-reduced-motion:\s*reduce\)[\s\S]*\.workbench-ai-action,[\s\S]*\.workbench-ai-message,[\s\S]*\.workbench-ai-composer--inline,[\s\S]*\.workbench-ai-date-popover,[\s\S]*\.workbench-ai-thinking-dot\s*\{[\s\S]*animation:\s*none;/)
  assert.ok(statSync(orbIconAsset).size > 100 * 1024)
  assert.ok(statSync(orbIconAsset).size < 3 * 1024 * 1024)
  assert.ok(statSync(orbIconPngAsset).size > 100 * 1024)
  assert.equal(orbIconBuffer.subarray(0, 6).toString('ascii'), 'GIF89a')
  assert.ok(countGifFrameBlocks(orbIconBuffer) >= 120)
  const gifMotion = measureGifMotion(orbIconAsset)
  assert.ok(gifMotion.seamDelta > gifMotion.medianAdjacentDelta * 0.35)
  assert.ok(gifMotion.seamDelta < gifMotion.medianAdjacentDelta * 1.8)
  assert.ok(measureGifDuration(orbIconAsset) >= 8000)
  assert.ok(measureGifDuration(orbIconAsset) / countGifFrameBlocks(orbIconBuffer) <= 75)
  const gifPresentation = measureOrbAssetPresentation(orbIconAsset)
  assert.equal(gifPresentation.width, 192)
  assert.equal(gifPresentation.height, 192)
  assert.ok(gifPresentation.minimumCornerLuma > 225)
  assert.ok(gifPresentation.maximumCornerLuma < 250)
  assert.ok(gifPresentation.minimumBackgroundSimilarityRatio > 0.25)
  assert.ok(gifPresentation.minimumForegroundWidthRatio > 0.9)
  assert.ok(gifPresentation.minimumForegroundHeightRatio > 0.9)
  const pngPresentation = measureOrbAssetPresentation(orbIconPngAsset)
  assert.ok(pngPresentation.minimumCornerLuma > 225)
  assert.ok(pngPresentation.maximumCornerLuma < 250)
  assert.ok(pngPresentation.minimumBackgroundSimilarityRatio > 0.25)
  assert.ok(pngPresentation.minimumForegroundWidthRatio > 0.9)
  assert.ok(pngPresentation.minimumForegroundHeightRatio > 0.9)
})

test('AI attachment association notifies shell to refresh the target detail page', () => {
  const aiModeComponent = readSource('../src/components/business/PersonalWorkbenchAiMode.vue')
  const workbenchView = readSource('../src/views/PersonalWorkbenchView.vue')
  const appShellRouteView = readSource('../src/views/AppShellRouteView.vue')
  const aiModeComposable = readSource('../src/composables/workbenchAiMode/usePersonalWorkbenchAiMode.js')
  const attachmentFlow = readSource('../src/composables/workbenchAiMode/useWorkbenchAiAttachmentAssociationFlow.js')

  assert.match(aiModeComponent, /defineEmits\(\[[^\]]*'request-updated'/)
  assert.match(workbenchView, /@request-updated="emit\('request-updated', \$event\)"/)
  assert.match(workbenchView, /defineEmits\(\[[^\]]*'request-updated'/)
  assert.match(appShellRouteView, /<PersonalWorkbenchView[\s\S]*@request-updated="handleRequestUpdated"/)
  assert.match(
    aiModeComposable,
    /notifyRequestUpdated:\s*\(payload\)\s*=>\s*emit\('request-updated', payload\)/
  )
  assert.match(
    attachmentFlow,
    /notifyRequestUpdated\?\.\(\{[\s\S]*claimId:[\s\S]*runtime\.claimId[\s\S]*uploadedCount:[\s\S]*syncResult\?\.uploadedCount/
  )
})

test('AI mode normal assistant requests include OCR context for uploaded receipts', () => {
  assert.match(aiModeSurface, /function isLikelyAiModeOcrFile\(file = \{\}\)/)
  assert.match(aiModeSurface, /const aiModeReceiptContextCache = new Map\(\)/)
  assert.match(aiModeSurface, /const aiModeReceiptRecognitionState = reactive\(\{\}\)/)
  assert.match(aiModeSurface, /function resolveAiModeReceiptRecognitionState\(file\)/)
  assert.match(aiModeSurface, /function hasPendingAiModeReceiptRecognition\(files = \[\]\)/)
  assert.match(aiModeSurface, /function hasFailedAiModeReceiptRecognition\(files = \[\]\)/)
  assert.match(aiModeSurface, /resolveAiModeReceiptRecognitionState\(selectedFiles\.value\[index\]\)/)
  assert.match(aiModeSurface, /status:\s*'recognizing'[\s\S]*label:\s*'智能录入识别中'/)
  assert.match(aiModeSurface, /status:\s*'recognized'[\s\S]*label:\s*detail \? `当前会话已识别/)
  assert.match(aiModeSurface, /本状态不代表票据夹已有记录/)
  assert.match(aiModeSurface, /status:\s*'failed'[\s\S]*label:\s*'识别失败'/)
  assert.match(aiModeSurface, /const isAiModeReceiptRecognitionPending = computed\(\(\) => attachmentFlow\.hasPendingAiModeReceiptRecognition\(selectedFiles\.value\)\)/)
  assert.match(aiModeSurface, /const hasAiModeReceiptRecognitionFailure = computed\(\(\) => attachmentFlow\.hasFailedAiModeReceiptRecognition\(selectedFiles\.value\)\)/)
  assert.match(aiModeSurface, /const isAiModeInputLocked = computed\(\(\) => applicationPreviewEstimatePending\.value \|\| isAiModeReceiptRecognitionPending\.value\)/)
  assert.match(aiModeSurface, /!hasAiModeReceiptRecognitionFailure\.value[\s\S]*Boolean\(assistantDraft\.value\.trim\(\)\)/)
  assert.match(aiModeSurface, /function resolveAiModeInputLockMessage\(\) \{[\s\S]*附件识别中，请稍等/)
  assert.match(aiModeSurface, /hasAiModeReceiptRecognitionFailure\.value[\s\S]*请先移除识别失败的附件或重新上传/)
  assert.match(aiModeSurface, /:placeholder="runtime\.isAiModeInputLocked \? runtime\.aiModeInputLockMessage : placeholder"/)
  assert.match(aiModeSurface, /function primeAiModeReceiptContext\(files = \[\]\)/)
  assert.match(aiModeSurface, /function startAiModeReceiptRecognition\(files = \[\], options = \{\}\)/)
  assert.match(aiModeSurface, /const forceRefresh = Boolean\(options\.forceRefresh\)/)
  assert.match(aiModeSurface, /if \(!forceRefresh && cached\?\.status === 'resolved'\) \{/)
  assert.match(aiModeSurface, /startAiModeReceiptRecognition\(files, \{ forceRefresh: true \}\)/)
  assert.match(aiModeSurface, /function buildAiModeReceiptContextCacheKey\(ocrFiles = \[\]\)/)
  assert.match(aiModeSurface, /applyAiModeReceiptRecognitionResult\(ocrFiles, context\)/)
  assert.match(aiModeSurface, /buildFileIdentity\(file\)/)
  assert.match(aiModeSurface, /watch\(selectedFiles, \(files(?:, previousFiles = \[\])?\) => \{[\s\S]*attachmentFlow\.primeAiModeReceiptContext\(files\)/)
  assert.match(aiModeSurface, /async function collectAiModeReceiptContext\(files = \[\]\)/)
  assert.match(aiModeSurface, /cached\?\.status === 'pending'[\s\S]*await cached\.promise/)
  assert.match(aiModeSurface, /collectReceiptFiles\(\{[\s\S]*files:\s*ocrFiles,[\s\S]*recognizeOcrFiles[\s\S]*\}\)/)
  assert.match(aiModeSurface, /const receiptContext = await collectAiModeReceiptContext\(files\)/)
  assert.match(aiModeSurface, /const attachmentOcrDetails = buildInlineAttachmentOcrDetails\(receiptContext, files\)/)
  assert.match(aiModeSurface, /ocr_summary:\s*receiptContext\.ocrSummary/)
  assert.match(aiModeSurface, /ocr_documents:\s*receiptContext\.ocrDocuments/)
  assert.match(aiModeSurface, /attachment_names:\s*receiptContext\.attachmentNames/)
  assert.match(aiModeSurface, /attachment_count:\s*receiptContext\.attachmentCount/)
  assert.match(aiModeSurface, /ocr_source_file_names:\s*receiptContext\.ocrSourceFileNames/)
  assert.match(aiModeSurface, /attachmentOcrDetails/)
})
