import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import test from 'node:test'
import { fileURLToPath } from 'node:url'
import { ref } from 'vue'

import {
  buildReviewFormContextFromPayload,
  buildLocallySyncedReviewPayload,
  buildReviewNextStepRichCopy,
  buildReviewPlainFollowupCopy,
  isTravelReviewPayload,
  resolveReviewFooterActions
} from '../src/views/scripts/travelReimbursementReviewModel.js'
import { useTravelReimbursementAttachments } from '../src/views/scripts/useTravelReimbursementAttachments.js'
import { renderMarkdown } from '../src/utils/markdown.js'

const createViewTemplate = readFileSync(
  fileURLToPath(new URL('../src/views/TravelReimbursementCreateView.vue', import.meta.url)),
  'utf8'
)
const createViewScript = readFileSync(
  fileURLToPath(new URL('../src/views/scripts/TravelReimbursementCreateView.js', import.meta.url)),
  'utf8'
)
const createViewScriptSurface = [
  '../src/views/scripts/TravelReimbursementCreateView.js',
  '../src/views/scripts/useTravelReimbursementComposerTools.js',
  '../src/views/scripts/useTravelReimbursementCreateViewControls.js',
  '../src/views/scripts/useTravelReimbursementCreateViewDrawerControls.js',
  '../src/views/scripts/useTravelReimbursementCreateViewLifecycle.js',
  '../src/views/scripts/useTravelReimbursementCreateViewMessageHandlers.js',
  '../src/views/scripts/useTravelReimbursementCreateViewState.js',
  '../src/views/scripts/useTravelReimbursementCreateViewTravelCalculator.js',
  '../src/views/scripts/useTravelReimbursementCreateViewUi.js',
  '../src/views/scripts/useTravelReimbursementMessageActions.js',
  '../src/views/scripts/useTravelReimbursementReviewActions.js'
].map((path) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')).join('\n')
const reviewPanelModelScript = readFileSync(
  fileURLToPath(new URL('../src/views/scripts/travelReimbursementReviewPanelModel.js', import.meta.url)),
  'utf8'
)
const createReviewModelScript = readFileSync(
  fileURLToPath(new URL('../src/views/scripts/travelReimbursementCreateReviewModel.js', import.meta.url)),
  'utf8'
)
const messageItemTemplate = readFileSync(
  fileURLToPath(new URL('../src/components/travel/TravelReimbursementMessageItem.vue', import.meta.url)),
  'utf8'
)
const insightPanelTemplate = readFileSync(
  fileURLToPath(new URL('../src/components/travel/TravelReimbursementInsightPanel.vue', import.meta.url)),
  'utf8'
)
const createViewTemplateSurface = [
  createViewTemplate,
  messageItemTemplate,
  insightPanelTemplate
].join('\n')
const reimbursementService = readFileSync(
  fileURLToPath(new URL('../src/services/reimbursements.js', import.meta.url)),
  'utf8'
)
const reimbursementFlowScript = readFileSync(
  fileURLToPath(new URL('../src/views/scripts/useTravelReimbursementFlow.js', import.meta.url)),
  'utf8'
)
const flowTimingScript = readFileSync(
  fileURLToPath(new URL('../src/views/scripts/travelReimbursementFlowTiming.js', import.meta.url)),
  'utf8'
)
const reviewActionsScript = readFileSync(
  fileURLToPath(new URL('../src/views/scripts/useTravelReimbursementReviewActions.js', import.meta.url)),
  'utf8'
)
const reviewDrawerScript = readFileSync(
  fileURLToPath(new URL('../src/views/scripts/useTravelReimbursementReviewDrawer.js', import.meta.url)),
  'utf8'
)
const submitComposerScript = [
  '../src/views/scripts/travelReimbursementSubmitConstants.js',
  '../src/views/scripts/travelReimbursementSubmitApplicationConflicts.js',
  '../src/views/scripts/travelReimbursementSubmitApplicationPreview.js',
  '../src/views/scripts/travelReimbursementSubmitLocalPreviewFlow.js',
  '../src/views/scripts/travelReimbursementSubmitStewardDelegation.js',
  '../src/views/scripts/travelReimbursementSubmitAttachmentFlow.js',
  '../src/views/scripts/travelReimbursementSubmitDraftPreflight.js',
  '../src/views/scripts/travelReimbursementSubmitRecognitionFlow.js',
  '../src/views/scripts/travelReimbursementSubmitResponseModel.js',
  '../src/views/scripts/useTravelReimbursementSubmitComposer.js'
].map((path) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')).join('\n')
const attachmentsScript = readFileSync(
  fileURLToPath(new URL('../src/views/scripts/useTravelReimbursementAttachments.js', import.meta.url)),
  'utf8'
)
const attachmentSyncScript = readFileSync(
  fileURLToPath(new URL('../src/utils/expenseClaimAttachmentSync.js', import.meta.url)),
  'utf8'
)
const sessionStateScript = readFileSync(
  fileURLToPath(new URL('../src/views/scripts/useTravelReimbursementSessionState.js', import.meta.url)),
  'utf8'
)
const createViewBaseStyles = readFileSync(
  fileURLToPath(new URL('../src/assets/styles/views/travel-reimbursement-create-view.css', import.meta.url)),
  'utf8'
)
const createViewPart2Styles = readFileSync(
  fileURLToPath(new URL('../src/assets/styles/views/travel-reimbursement-create-view-part2.css', import.meta.url)),
  'utf8'
)
const createViewPart3Styles = readFileSync(
  fileURLToPath(new URL('../src/assets/styles/views/travel-reimbursement-create-view-part3.css', import.meta.url)),
  'utf8'
)
const createViewPart4Styles = readFileSync(
  fileURLToPath(new URL('../src/assets/styles/views/travel-reimbursement-create-view-part4.css', import.meta.url)),
  'utf8'
)
const insightPanelStyles = readFileSync(
  fileURLToPath(new URL('../src/assets/styles/components/travel-reimbursement-insight-panel.css', import.meta.url)),
  'utf8'
)

test('review drawer tools expose the default review tab before conditional document and risk tabs', () => {
  assert.match(createViewTemplateSurface, /v-if="ui\.activeReviewPayload && ui\.reviewOverviewDrawerAvailable"[\s\S]*title="报销识别核对"[\s\S]*@click="ui\.switchToReviewOverviewDrawer"/)
  assert.match(createViewTemplateSurface, /v-if="ui\.activeReviewPayload && ui\.reviewDocumentDrawerAvailable"[\s\S]*title="单据识别"/)
  assert.match(createViewTemplateSurface, /v-if="ui\.activeReviewPayload && ui\.reviewRiskDrawerAvailable"[\s\S]*title="显示风险"/)
  assert.match(createViewTemplateSurface, /title="调用流程"/)

  assert.ok(
    createViewTemplateSurface.indexOf('title="报销识别核对"') < createViewTemplateSurface.indexOf('title="单据识别"'),
    'default review button should be placed before the document recognition button'
  )
})

test('create review model remains a thin compatibility layer over review panel model', () => {
  assert.match(createReviewModelScript, /export \{[\s\S]*buildReviewFactCards[\s\S]*buildReviewRiskItems[\s\S]*\} from '\.\/travelReimbursementReviewPanelModel\.js'/)
  assert.doesNotMatch(createReviewModelScript, /function buildReviewFactCards/)
  assert.doesNotMatch(createReviewModelScript, /function buildReviewRiskItems/)
  assert.doesNotMatch(createReviewModelScript, /const REVIEW_RISK_LEVEL_META/)
})

test('review drawer tool buttons switch modes instead of toggling the active mode closed', () => {
  assert.match(createViewScriptSurface, /const isReviewOverviewDrawer = computed\(\(\) => reviewDrawerMode\.value === REVIEW_DRAWER_MODE_REVIEW\)/)
  assert.match(createViewScriptSurface, /function switchReviewDrawerMode\(mode\) \{[\s\S]*if \(reviewDrawerMode\.value === mode\) \{[\s\S]*return[\s\S]*\}/)
  assert.match(createViewScriptSurface, /function switchToReviewOverviewDrawer\(\) \{[\s\S]*switchReviewDrawerMode\(REVIEW_DRAWER_MODE_REVIEW\)/)
  assert.match(createViewScriptSurface, /function toggleReviewDocumentDrawer\(\) \{[\s\S]*switchReviewDrawerMode\(REVIEW_DRAWER_MODE_DOCUMENTS\)/)
  assert.match(createViewScriptSurface, /function toggleReviewRiskDrawer\(\) \{[\s\S]*switchReviewDrawerMode\(REVIEW_DRAWER_MODE_RISK\)/)
  assert.match(createViewScriptSurface, /function toggleReviewFlowDrawer\(\) \{[\s\S]*switchReviewDrawerMode\(REVIEW_DRAWER_MODE_FLOW\)/)
  assert.doesNotMatch(createViewScriptSurface, /REVIEW_DRAWER_MODE_DOCUMENTS\s*\?\s*REVIEW_DRAWER_MODE_REVIEW/)
  assert.doesNotMatch(createViewScriptSurface, /REVIEW_DRAWER_MODE_RISK\s*\?\s*REVIEW_DRAWER_MODE_REVIEW/)
  assert.doesNotMatch(createViewScriptSurface, /REVIEW_DRAWER_MODE_FLOW\s*\?\s*REVIEW_DRAWER_MODE_REVIEW/)
})

test('document review drawer fills sidebar height and preview dialog is centered', () => {
  assert.match(createViewTemplateSurface, /class="insight-body"[\s\S]*:class="\{ 'document-review-body': ui\.isReviewDocumentDrawer \}"/)
  assert.match(createViewBaseStyles, /\.insight-panel-shell\s*\{[\s\S]*display:\s*flex;[\s\S]*min-height:\s*0;/)
  assert.match(createViewPart2Styles, /\.insight-body\.document-review-body\s*\{[\s\S]*display:\s*flex;[\s\S]*overflow:\s*hidden;/)
  assert.match(createViewPart2Styles, /\.review-ticket-drawer\s*\{[\s\S]*grid-template-rows:\s*auto minmax\(0,\s*1fr\);[\s\S]*height:\s*100%;[\s\S]*overflow:\s*hidden;/)
  assert.match(createViewPart2Styles, /\.review-document-stage\s*\{[\s\S]*grid-template-rows:\s*auto auto minmax\(0,\s*1fr\);/)
  assert.match(createViewPart2Styles, /\.review-document-scroll\s*\{[\s\S]*max-height:\s*none;[\s\S]*min-height:\s*0;/)
  assert.match(createViewPart2Styles, /\.review-document-preview-card\.image\s*\{[\s\S]*place-items:\s*center;[\s\S]*min-height:\s*220px;/)
  assert.match(createViewPart2Styles, /\.review-document-preview-card\.image img\s*\{[\s\S]*height:\s*auto;[\s\S]*object-fit:\s*contain;/)
  assert.doesNotMatch(createViewPart2Styles, /\.review-document-preview-card\.image img\s*\{[\s\S]*object-fit:\s*cover;/)
  assert.match(createViewPart4Styles, /\.review-overlay\s*\{[\s\S]*align-items:\s*center;[\s\S]*justify-content:\s*center;/)
  assert.match(createViewPart4Styles, /\.review-preview-modal\s*\{[\s\S]*margin:\s*auto;[\s\S]*flex:\s*none;/)
})

test('assistant conversation keeps composer visible when generated cards grow tall', () => {
  assert.match(createViewBaseStyles, /\.assistant-layout\s*\{[\s\S]*height:\s*100%;[\s\S]*max-height:\s*100%;[\s\S]*overflow:\s*hidden;/)
  assert.match(
    createViewBaseStyles,
    /\.dialog-panel\s*\{[\s\S]*display:\s*flex;[\s\S]*flex-direction:\s*column;[\s\S]*height:\s*100%;[\s\S]*max-height:\s*100%;[\s\S]*min-height:\s*0;[\s\S]*overflow:\s*hidden;/
  )
  assert.match(createViewBaseStyles, /\.dialog-toolbar\s*\{[\s\S]*flex:\s*0 0 auto;/)
  assert.match(
    createViewBaseStyles,
    /\.message-list\s*\{[\s\S]*flex:\s*1 1 0;[\s\S]*min-height:\s*0;[\s\S]*max-height:\s*100%;[\s\S]*overflow-y:\s*auto;[\s\S]*overscroll-behavior:\s*contain;/
  )
  assert.match(createViewBaseStyles, /\.composer\s*\{[\s\S]*position:\s*sticky;[\s\S]*bottom:\s*0;[\s\S]*flex:\s*0 0 auto;[\s\S]*flex-shrink:\s*0;/)
  assert.match(createViewPart4Styles, /@media \(max-width:\s*1440px\)[\s\S]*\.dialog-panel\s*\{[\s\S]*flex:\s*1 1 0;[\s\S]*height:\s*auto;[\s\S]*max-height:\s*100%;/)
})

test('document review OCR result card header keeps copy and navigation separated', () => {
  assert.match(insightPanelTemplate, /class="review-side-head-copy"[\s\S]*票据识别结果卡片[\s\S]*逐张查看 OCR 结果/)
  assert.match(insightPanelStyles, /\.review-document-switch-head\s*\{[\s\S]*display:\s*grid;[\s\S]*grid-template-columns:\s*minmax\(0,\s*1fr\) auto;/)
  assert.match(insightPanelStyles, /\.review-side-head-copy\s*\{[\s\S]*min-width:\s*0;[\s\S]*display:\s*grid;/)
  assert.match(insightPanelStyles, /\.review-side-head-copy p\s*\{[\s\S]*overflow-wrap:\s*anywhere;/)
  assert.match(insightPanelStyles, /\.review-document-nav\s*\{[\s\S]*flex:\s*0 0 auto;[\s\S]*flex-wrap:\s*nowrap;[\s\S]*white-space:\s*nowrap;/)
})

test('document preview avoids restored stale object urls', () => {
  assert.match(createViewTemplateSurface, /v-if="documentPreviewDialog\.kind === 'image'"[\s\S]*:key="documentPreviewDialog\.renderKey"/)
  assert.match(createViewTemplateSurface, /v-else-if="documentPreviewDialog\.kind === 'pdf'"[\s\S]*:key="documentPreviewDialog\.renderKey"/)
  assert.match(reviewDrawerScript, /renderKey:\s*''/)
  assert.match(reviewDrawerScript, /renderKey:\s*\[[\s\S]*Date\.now\(\)[\s\S]*\]\.join\('__'\)/)
  assert.match(attachmentsScript, /isTemporaryPreviewUrl/)
  assert.match(attachmentsScript, /existingPreview\?\.url && !isTemporaryPreviewUrl\(existingPreview\.url\)/)
  assert.match(sessionStateScript, /filterPersistableFilePreviews\(state\.reviewFilePreviews\)/)
  assert.doesNotMatch(sessionStateScript, /filterPersistableFilePreviews\(nextState\.reviewFilePreviews\)/)
})

test('local transport review no longer uses the travel hotel template', () => {
  const reviewPayload = {
    slot_cards: [
      {
        key: 'expense_type',
        label: '报销类型',
        value: '交通费',
        normalized_value: 'transport',
        status: 'identified'
      }
    ],
    document_cards: [
      {
        document_type: 'taxi_receipt',
        suggested_expense_type: 'transport',
        scene_label: '交通费'
      }
    ]
  }

  assert.equal(isTravelReviewPayload(reviewPayload, { expense_type: '交通费' }), false)
  assert.match(reviewPanelModelScript, /shouldShowReviewFactCard\(reviewPayload, 'customer_name'/)
  assert.doesNotMatch(
    reviewPanelModelScript,
    /key:\s*'customer_name'[\s\S]{0,220}placeholder:\s*'请输入客户名称'[\s\S]{0,80}\},\s*\{[\s\S]{0,80}key:\s*'attachments'/
  )
  assert.match(createViewTemplateSurface, /placeholder="例如：出租车\/网约车票据 \/ 火车\/高铁票"/)
  assert.doesNotMatch(createViewTemplateSurface, /票据场景[\s\S]{0,260}例如：业务招待费 \/ 差旅费/)
})

test('local save of changed reimbursement category updates edit fields too', () => {
  const nextPayload = buildLocallySyncedReviewPayload(
    {
      can_proceed: false,
      edit_fields: [
        { key: 'expense_type', label: '报销分类', value: '交通费' },
        { key: 'reason', label: '事由', value: '打车去客户现场' }
      ],
      slot_cards: [
        {
          key: 'expense_type',
          label: '报销类型',
          value: '交通费',
          normalized_value: 'transport',
          required: true,
          status: 'identified'
        }
      ],
      confirmation_actions: []
    },
    {
      expense_type: '办公用品费',
      reason_value: '右侧核对后改为办公用品费'
    }
  )

  const expenseTypeField = nextPayload.edit_fields.find((item) => item.key === 'expense_type')
  assert.equal(expenseTypeField.value, '办公用品费')
  assert.equal(nextPayload.slot_cards[0].value, '办公用品费')
})

test('next step action uses rich text guidance and confirm dialog instead of footer button', () => {
  const reviewPayload = {
    can_proceed: true,
    risk_briefs: [
      { level: 'low', title: '票据提示', content: '普通提示' }
    ],
    confirmation_actions: [
      { action_type: 'save_draft', label: '保存为草稿' },
      { action_type: 'next_step', label: '继续下一步', emphasis: 'primary' }
    ]
  }
  const copy = buildReviewNextStepRichCopy(reviewPayload, { detailHref: '/app/documents/claim-1' })
  const rendered = renderMarkdown(copy)

  assert.match(copy, /系统识别您的单据已经填写完所有已知信息/)
  assert.match(copy, /现存在 1 条低风险，0 条中风险，0 条高风险/)
  assert.doesNotMatch(copy, /#review-risk-low/)
  assert.doesNotMatch(copy, /#review-risk-medium/)
  assert.doesNotMatch(copy, /#review-risk-high/)
  assert.match(copy, /\[右侧\]\(#review-risk-panel\) 风险信息提示窗/)
  assert.match(copy, /\[继续下一步\]\(#review-next-step\)/)
  assert.match(copy, /\[快速修改单据信息\]\(\/app\/documents\/claim-1\)/)
  assert.doesNotMatch(rendered, /markdown-risk-link-/)
  assert.match(rendered, /<span class="markdown-risk-text-low">低风险<\/span>/)
  assert.match(rendered, /<span class="markdown-risk-text-medium">中风险<\/span>/)
  assert.match(rendered, /<span class="markdown-risk-text-high">高风险<\/span>/)
  assert.doesNotMatch(rendered, /href="#review-risk-low"/)
  assert.doesNotMatch(rendered, /href="#review-risk-medium"/)
  assert.doesNotMatch(rendered, /href="#review-risk-high"/)
  assert.match(rendered, /markdown-action-link-risk/)
  assert.match(rendered, /markdown-action-link-next/)
  assert.deepEqual(resolveReviewFooterActions(reviewPayload), [])

  const highRiskCopy = buildReviewNextStepRichCopy(
    {
      ...reviewPayload,
      risk_briefs: [{ level: 'high', title: '金额超标' }]
    },
    { detailHref: '/app/documents/claim-1' }
  )
  assert.doesNotMatch(highRiskCopy, /\[继续下一步\]\(#review-next-step\)/)

  assert.match(createViewTemplateSurface, /class="review-next-step-rich-copy message-answer-markdown"[\s\S]*ui\.renderMarkdown\(ui\.buildReviewNextStepRichCopyForMessage\(message\)\)/)
  assert.match(createViewTemplateSurface, /class="message-bubble"[\s\S]*:class="ui\.buildMessageBubbleClass\(message\)"/)
  assert.match(createViewTemplateSurface, /:open="nextStepConfirmDialog\.open"[\s\S]*title="确认提交当前单据？"[\s\S]*confirm-text="确认提交"/)
  assert.match(createViewScriptSurface, /const REVIEW_NEXT_STEP_HREF = '#review-next-step'/)
  assert.match(createViewScriptSurface, /buildReviewRiskLevelCounts/)
  assert.match(createViewScriptSurface, /function buildMessageBubbleClass\(message\)/)
  assert.match(createViewScriptSurface, /message-bubble-review-risk-high/)
  assert.match(createViewScriptSurface, /message-bubble-review-risk-medium/)
  assert.match(createViewScriptSurface, /message-bubble-review-risk-low/)
  assert.match(createViewScriptSurface, /function openReviewNextStepConfirm\(message\)/)
  assert.match(createViewScriptSurface, /async function confirmReviewNextStepSubmit\(\)/)
  assert.match(createViewScriptSurface, /href === REVIEW_NEXT_STEP_HREF[\s\S]*openReviewNextStepConfirm\(message\)/)
  assert.match(createViewScriptSurface, /href\.startsWith\(REVIEW_RISK_PANEL_HREF_PREFIX\)[\s\S]*switchReviewDrawerMode\(REVIEW_DRAWER_MODE_RISK\)/)
  assert.match(createViewBaseStyles, /\.message-bubble-review-risk-low\s*\{[\s\S]*border-color:\s*rgba\(37,\s*99,\s*235,/)
  assert.match(createViewBaseStyles, /\.message-bubble-review-risk-medium\s*\{[\s\S]*border-color:\s*rgba\(217,\s*119,\s*6,/)
  assert.match(createViewBaseStyles, /\.message-bubble-review-risk-high\s*\{[\s\S]*border-color:\s*rgba\(220,\s*38,\s*38,/)
  assert.doesNotMatch(createViewBaseStyles, /markdown-risk-link-low/)
  assert.match(createViewBaseStyles, /\.markdown-risk-text-low[\s\S]*color:\s*#2563eb/)
  assert.match(createViewBaseStyles, /\.markdown-risk-text-medium[\s\S]*color:\s*#d97706/)
  assert.match(createViewBaseStyles, /\.markdown-risk-text-high[\s\S]*color:\s*#dc2626/)
  assert.match(createViewPart3Styles, /\.review-next-step-rich-copy\s*\{[\s\S]*margin-top:\s*30px;/)
})

test('review risk drawer lists risk briefs without score and posts details into the conversation', () => {
  const riskItemsBlock = reviewPanelModelScript.match(/function buildReviewRiskItems\(reviewPayload\) \{[\s\S]*?\n\}\n\nexport function buildReviewRiskConversationText/)
  assert.ok(riskItemsBlock, 'risk item builder should be present')

  assert.doesNotMatch(createViewTemplateSurface, /review-side-risk-score/)
  assert.doesNotMatch(createViewTemplateSurface, /风险评分/)
  assert.doesNotMatch(createViewTemplateSurface, /暂无风险评分/)
  assert.doesNotMatch(createViewScriptSurface, /function buildReviewRiskScore/)
  assert.doesNotMatch(createViewScriptSurface, /const reviewRiskScore/)
  assert.doesNotMatch(riskItemsBlock[0], /\.slice\(0,\s*6\)/)
  assert.match(reviewPanelModelScript, /const DEPRECATED_REVIEW_RISK_TITLE_KEYWORDS = \[[\s\S]*'历史报销画像'[\s\S]*'制度注意事项'/)
  assert.match(
    reviewPanelModelScript,
    /function resolveReviewRiskBriefs\(reviewPayload\) \{[\s\S]*DEPRECATED_REVIEW_RISK_TITLE_KEYWORDS\.some/
  )

  assert.match(
    createViewTemplateSurface,
    /class="review-side-risk-item"[\s\S]*@click="ui\.appendReviewRiskBriefToConversation\(item\)"/
  )
  assert.doesNotMatch(createViewTemplateSurface, /\{\{\s*item\.levelLabel\s*\}\}/)
  assert.match(createViewTemplateSurface, /class="review-side-risk-icon" :title="item\.levelLabel"/)
  assert.match(reviewPanelModelScript, /info:\s*\{[\s\S]*label:\s*'提示'/)
  assert.match(reviewPanelModelScript, /medium:\s*\{[\s\S]*label:\s*'中风险'/)
  assert.match(reviewPanelModelScript, /low:\s*\{[\s\S]*label:\s*'低风险'/)
  assert.match(reviewPanelModelScript, /const isInfo = String\(item\?\.level \|\| ''\)\.trim\(\) === 'info'/)
  assert.match(reviewPanelModelScript, /\$\{isInfo \? '提示内容' : '风险点'\}：\$\{summary\}/)
  assert.match(reviewPanelModelScript, /\$\{isInfo \? '处理建议' : '修改建议'\}：\$\{suggestion\}/)
  assert.match(reviewPanelModelScript, /function normalizeReviewRiskTitle/)
  assert.match(reviewPanelModelScript, /\.replace\(\/AI\\s\*预审/)
  assert.match(reviewPanelModelScript, /\.replace\(\/\(高风险\|中风险\|低风险\)\/g,\s*''\)/)
  assert.match(reviewPanelModelScript, /sourceLabel:\s*meta\.label/)
  assert.doesNotMatch(reviewPanelModelScript, /normalizedTitle\.includes\('AI预审'\)/)
  assert.match(createViewScriptSurface, /metaTone:\s*item\.level \|\| 'low'/)
  assert.doesNotMatch(createViewTemplateSurface, /@click="openReviewRiskDetail\(item\)"/)
  assert.doesNotMatch(createViewTemplateSurface, /review-risk-detail-modal/)
  assert.doesNotMatch(createViewScriptSurface, /reviewRiskDetailDialog/)
  assert.doesNotMatch(createViewScriptSurface, /function openReviewRiskDetail/)

  assert.match(
    createViewScriptSurface,
    /function appendReviewRiskBriefToConversation\(item\) \{[\s\S]*messages\.value\.push\(createMessage\('assistant'/
  )
  assert.match(reviewPanelModelScript, /function buildReviewRiskConversationText\(item, detailTarget = \{\}\)/)
  assert.match(createViewScriptSurface, /function resolveReviewDetailTarget\(message = null\) \{[\s\S]*router\.resolve\(\{[\s\S]*name: 'app-document-detail'/)
  assert.match(createViewScriptSurface, /function resolveReviewRiskDetailTarget\(\) \{[\s\S]*return resolveReviewDetailTarget\(\)/)
  assert.match(createViewScriptSurface, /进入 \$\{claimNo\} 详情重新填写/)
  assert.match(createViewTemplateSurface, /class="expense-query-risk-row"[\s\S]*ui\.appendExpenseQueryRiskToConversation\(record, risk\)/)
  assert.match(createViewScriptSurface, /function appendExpenseQueryRiskToConversation\(record, risk\) \{[\s\S]*进入 \$\{claimNo\} 详情重新填写/)
})

test('review drawer default mode is scoped by the current action and travel overview uses travel-specific fields', () => {
  assert.match(reviewDrawerScript, /activeReviewPanelScope/)
  assert.match(reviewDrawerScript, /const reviewOverviewDrawerAvailable = computed\(\(\) => normalizedReviewPanelScope\.value === 'overview'\)/)
  assert.match(reviewDrawerScript, /scope === 'documents' && hasDocuments[\s\S]*REVIEW_DRAWER_MODE_DOCUMENTS/)
  assert.match(reviewDrawerScript, /scope === 'risk' && hasRisks[\s\S]*REVIEW_DRAWER_MODE_RISK/)
  assert.match(reviewDrawerScript, /scope === 'overview'[\s\S]*REVIEW_DRAWER_MODE_REVIEW/)
  assert.match(reviewPanelModelScript, /function normalizeReviewPanelScope\(scope\)/)
  assert.match(createViewScriptSurface, /canExposeReviewPanelScope\(item\.reviewPanelScope\)/)
  assert.match(createViewScriptSurface, /currentInsight\.value\.intent === 'agent' && agent[\s\S]*return null/)
  assert.match(reviewPanelModelScript, /function isTravelReviewPayload\(reviewPayload/)
  assert.match(reviewPanelModelScript, /function resolveReviewTravelTransportType\(reviewPayload/)
  assert.match(reviewPanelModelScript, /label: '交通类型'[\s\S]*modelKey: 'transport_type'/)
  assert.match(reviewPanelModelScript, /label: '酒店名称'[\s\S]*modelKey: 'merchant_name'/)
  assert.match(reviewPanelModelScript, /label: '出差事宜'[\s\S]*editor: 'textarea'[\s\S]*wide: true/)
  assert.match(createViewTemplateSurface, /item\.editor === 'textarea'[\s\S]*<textarea/)
  assert.match(createViewTemplateSurface, /wide: item\.wide/)
})

test('submit composer scopes the side panel to intent overview, document upload, or triggered risk only', () => {
  assert.match(submitComposerScript, /function resolveReviewPanelScope\(\{[\s\S]*reviewPayload = null/)
  assert.match(submitComposerScript, /fileCount > 0 && documentCount > 0[\s\S]*return 'documents'/)
  assert.match(submitComposerScript, /riskCount > 0 && \(asksRisk \|\| \['next_step', 'submit', 'submit_claim'\]\.includes\(normalizedAction\)\)[\s\S]*return 'risk'/)
  assert.match(submitComposerScript, /!normalizedAction && fileCount === 0[\s\S]*return 'overview'/)
  assert.match(submitComposerScript, /reviewPanelScope: stewardDelegated[\s\S]*resolveReviewPanelScope\(\{/)
  assert.match(submitComposerScript, /nextInsight\.agent\.reviewPanelScope = assistantMessage\.reviewPanelScope/)
})

test('expense query answers keep one clear result structure with document center jump link', () => {
  assert.doesNotMatch(createViewTemplateSurface, /message\.meta\?\.length/)
  assert.match(createViewTemplateSurface, /!message\.reviewPayload && \(!message\.queryPayload \|\| message\.queryPayload\.selectionMode === 'reimbursement_application_association'\) && message\.suggestedActions\?\.length/)
  assert.match(createViewTemplateSurface, /!message\.reviewPayload && !message\.queryPayload && message\.citations\?\.length/)
  assert.match(createViewTemplateSurface, /message\.queryPayload\.title \|\| \(message\.queryPayload\.selectionMode === 'draft_association' \? '选择关联草稿' : '最近 5 条筛选结果'\)/)
  assert.match(createViewTemplateSurface, /v-html="ui\.renderMarkdown\(ui\.buildExpenseQueryHint\(message\.queryPayload\)\)"/)
  assert.match(createViewScriptSurface, /href\.startsWith\('\/app\/'\)[\s\S]*router\.push\(href\)/)
})

test('backend query response suppresses generic query actions and supports archived filter title', () => {
  const responseScript = readFileSync(
    fileURLToPath(new URL('../../server/src/app/services/user_agent_response.py', import.meta.url)),
    'utf8'
  )
  const queryScript = readFileSync(
    fileURLToPath(new URL('../../server/src/app/services/orchestrator_expense_query.py', import.meta.url)),
    'utf8'
  )

  assert.match(responseScript, /if payload\.ontology\.intent in \{"query", "compare"\}:[\s\S]*return \[\]/)
  assert.match(responseScript, /下面先列出最近 \{query_payload\.preview_count\} 条记录/)
  assert.match(queryScript, /EXPENSE_QUERY_PREVIEW_LIMIT = 5/)
  assert.match(queryScript, /"归档"[\s\S]*"archived"/)
  assert.match(queryScript, /ExpenseClaim\.approval_stage\.ilike\("%归档%"\)/)
  assert.match(queryScript, /"title": \([\s\S]*f"最近 \{len\(preview_claims\)\} 条\{scope_label\}"/)
})

test('closing the assistant while OCR is running defers unmount until the current flow finishes', () => {
  assert.match(createViewScriptSurface, /const closeAfterBusy = ref\(false\)/)
  assert.match(createViewScriptSurface, /function isWorkbenchBusy\(\) \{[\s\S]*submitting\.value \|\| reviewActionBusy\.value \|\| sessionSwitchBusy\.value/)
  assert.match(createViewScriptSurface, /function maybeFinalizeDeferredClose\(\) \{[\s\S]*!closeAfterBusy\.value \|\| workbenchVisible\.value \|\| isWorkbenchBusy\(\)/)
  assert.match(createViewScriptSurface, /function requestCloseWorkbench\(\) \{[\s\S]*closeAfterBusy\.value = isWorkbenchBusy\(\)[\s\S]*workbenchVisible\.value = false/)
  assert.match(createViewScriptSurface, /function emitCloseAfterLeave\(\) \{[\s\S]*closeAfterBusy\.value && isWorkbenchBusy\(\)[\s\S]*return/)
  assert.match(createViewScriptSurface, /\[submitting\.value, reviewActionBusy\.value, sessionSwitchBusy\.value, workbenchVisible\.value\][\s\S]*maybeFinalizeDeferredClose\(\)/)
})

test('composer exposes travel calculator and posts spreadsheet-backed result into conversation', () => {
  assert.match(createViewTemplateSurface, /v-if="canShowTravelCalculator" class="travel-calculator-anchor"/)
  assert.match(createViewTemplateSurface, /class="tool-btn composer-side-btn travel-calculator-trigger"[\s\S]*差旅计算器/)
  assert.match(createViewTemplateSurface, /class="travel-calculator-popover"[\s\S]*v-model="travelCalculatorForm\.days"[\s\S]*v-model="travelCalculatorForm\.location"/)
  assert.doesNotMatch(createViewTemplateSurface, /travel-calculator-modal/)
  assert.doesNotMatch(createViewTemplateSurface, /travelCalculatorResult\.total_amount/)
  assert.match(createViewScriptSurface, /calculateTravelReimbursement/)
  assert.match(createViewScriptSurface, /const canShowTravelCalculator = computed\(\(\) => activeSessionType\.value === SESSION_TYPE_EXPENSE\)/)
  assert.match(createViewScriptSurface, /function openTravelCalculator\(\) \{[\s\S]*!canShowTravelCalculator\.value[\s\S]*closeTravelCalculator\(\)/)
  assert.match(createViewScriptSurface, /function toggleTravelCalculator\(\)/)
  assert.match(createViewScriptSurface, /function toggleTravelCalculator\(\) \{[\s\S]*!canShowTravelCalculator\.value[\s\S]*closeTravelCalculator\(\)/)
  assert.match(createViewScriptSurface, /watch\(canShowTravelCalculator,[\s\S]*closeTravelCalculator\(\)/)
  assert.match(createViewScriptSurface, /function submitTravelCalculator\(\) \{[\s\S]*calculateTravelReimbursement\(\{[\s\S]*grade: String\(user\.grade/)
  assert.match(createViewScriptSurface, /根据您输入的地点和天数/)
  assert.match(createViewScriptSurface, /匹配到您要出差的地区为/)
  assert.match(createViewScriptSurface, /参考可报销合计/)
  assert.match(createViewScriptSurface, /住宿费：\$\{hotelRate\} × \$\{days\} = \$\{hotelAmount\} 元/)
  assert.match(createViewScriptSurface, /messages\.value\.push\(createMessage\('assistant', buildTravelCalculatorResultText\(payload\)/)
  assert.match(reimbursementService, /export function calculateTravelReimbursement\(payload = \{\}\) \{[\s\S]*\/reimbursements\/travel-calculator/)
})

test('continuing receipt upload preserves prior review form context', () => {
  assert.match(reviewPanelModelScript, /function buildReviewFormContextFromPayload\(reviewPayload, inlineState = null\)/)
  assert.match(reviewPanelModelScript, /function buildBusinessTimeContextFromReviewValues\(values = \{\}\)/)
  assert.match(
    submitComposerScript,
    /resolvedUploadDisposition === 'continue_existing'[\s\S]*buildReviewFormContextFromPayload\([\s\S]*activeReviewPayload\.value[\s\S]*reviewInlineForm\.value[\s\S]*extraContext\.review_form_values/s
  )
  assert.match(
    submitComposerScript,
    /inheritedReviewContext\.business_time_context[\s\S]*extraContext\.business_time_context = inheritedReviewContext\.business_time_context/s
  )
})

test('review form context emits ontology fields instead of local aliases', () => {
  const context = buildReviewFormContextFromPayload(
    {
      edit_fields: [
        { key: 'expense_type', value: '' },
        { key: 'occurred_date', value: '' },
        { key: 'transport_type', value: '' },
        { key: 'reason', value: '' },
        { key: 'amount', value: '' },
        { key: 'business_location', value: '' },
        { key: 'attachment_names', value: '' }
      ]
    },
    {
      expense_type: '差旅费',
      occurred_date: '2026-06-01 至 2026-06-03',
      transport_type: '火车',
      reason_value: '支撑国网仿生产环境部署',
      location: '上海',
      amount: '3000',
      attachment_names: 'ticket.pdf'
    }
  )

  assert.equal(context.review_form_values.expense_type, '差旅费')
  assert.equal(context.review_form_values.time_range, '2026-06-01 至 2026-06-03')
  assert.equal(context.review_form_values.transport_mode, '火车')
  assert.equal(context.review_form_values.reason, '支撑国网仿生产环境部署')
  assert.equal(context.review_form_values.attachments, 'ticket.pdf')
  assert.equal(context.review_form_values.occurred_date, undefined)
  assert.equal(context.review_form_values.transport_type, undefined)
  assert.equal(context.review_form_values.reason_value, undefined)
})

test('review drawer save action is disabled while receipt recognition is submitting', () => {
  assert.match(createViewScriptSurface, /const submitting = ref\(false\)/)
  assert.match(
    submitComposerScript,
    /submitting\.value = true[\s\S]*handleSubmitRecognitionFlow\(\{[\s\S]*recognizeOcrFiles[\s\S]*submitting\.value = false/s
  )
  assert.match(
    submitComposerScript,
    /collectReceiptFiles\(\{[\s\S]*files,[\s\S]*recognizeOcrFiles[\s\S]*\}\)/
  )
  assert.match(
    createViewTemplateSurface,
    /class="review-side-save-pill"[\s\S]*:disabled="ui\.reviewActionBusy \|\| ui\.submitting"[\s\S]*@click="ui\.saveInlineReviewChanges"/
  )
  assert.match(
    createViewScriptSurface,
    /async function handleReviewAction\(message, action\) \{[\s\S]*if \(!actionType \|\| submitting\.value \|\| reviewActionBusy\.value \|\| sessionSwitchBusy\.value\) return/
  )
  assert.match(
    createViewScriptSurface,
    /function saveInlineReviewChanges\(\) \{[\s\S]*\|\| submitting\.value[\s\S]*\|\| sessionSwitchBusy\.value[\s\S]*\) return/
  )
})

test('flow run detail refresh has timeout so composer submit is not held open', () => {
  assert.match(flowTimingScript, /FLOW_RUN_DETAIL_REFRESH_TIMEOUT_MS\s*=\s*3000/)
  assert.match(
    reimbursementFlowScript,
    /await Promise\.race\(\[[\s\S]*fetchAgentRunDetail\(flowRunId\.value\)[\s\S]*globalThis\.setTimeout\(\(\) => resolve\(null\), FLOW_RUN_DETAIL_REFRESH_TIMEOUT_MS\)/
  )
  assert.match(reimbursementFlowScript, /if \(!run\) \{\s*return null\s*}/)
})

test('draft creation keeps detail-scoped attachment persistence alive before close', () => {
  assert.match(
    submitComposerScript,
    /const persistComposerFilesToDraft = async \(\) => \{[\s\S]*const syncResult = await syncComposerFilesToDraft\(resolvedDraftClaimId, files\)[\s\S]*persistSessionState\(\)[\s\S]*if \(detailScopedUpload\) \{[\s\S]*emitRequestUpdated\?\.\(\{/s
  )
  assert.match(
    submitComposerScript,
    /const persistTask = persistComposerFilesToDraft\(\)[\s\S]*if \(detailScopedUpload\) \{[\s\S]*await persistTask[\s\S]*\} else \{[\s\S]*void persistTask[\s\S]*\}/s
  )
  assert.ok(
    submitComposerScript.indexOf('replaceMessage(pendingMessage.id, assistantMessage)') <
      submitComposerScript.indexOf('const persistTask = persistComposerFilesToDraft()'),
    'assistant response should render before background attachment persistence starts'
  )
  assert.match(submitComposerScript, /source: 'detail-smart-entry-attachment-sync'/)
  assert.match(submitComposerScript, /uploadedCount: Number\(syncResult\?\.uploadedCount \|\| 0\)/)
  assert.match(attachmentSyncScript, /function normalizeAttachmentMatchName\(value\)/)
  assert.match(attachmentSyncScript, /const normalizedMatchBuckets = new Map\(\)/)
  assert.match(
    attachmentSyncScript,
    /nextExactMatch \|\| nextNormalizedMatch \|\| fallbackMatch \|\| emptyFallbackMatch/
  )
})

test('detail smart-entry receipt sync uploads files to existing empty items and creates a row when needed', async () => {
  const uploadCalls = []
  let createCount = 0
  const claimSnapshots = [
    {
      items: [
        { id: 'item-empty-1', item_type: 'hotel_ticket', invoice_id: '' },
        { id: 'item-persisted', item_type: 'train_ticket', invoice_id: 'claim-1/item-persisted/old.pdf' }
      ]
    },
    {
      items: [
        { id: 'item-empty-1', item_type: 'hotel_ticket', invoice_id: '' },
        { id: 'item-persisted', item_type: 'train_ticket', invoice_id: 'claim-1/item-persisted/old.pdf' }
      ]
    }
  ]

  const attachments = useTravelReimbursementAttachments({
    isKnowledgeSession: ref(false),
    reviewFilePreviews: ref([]),
    linkedRequest: ref({}),
    draftClaimId: ref('claim-1'),
    activeReviewPayload: ref(null),
    reviewInlinePendingFiles: ref([]),
    reviewInlineForm: ref({}),
    reviewInlineEditorKey: ref(''),
    composerUploadIntent: ref(''),
    submitting: ref(false),
    reviewActionBusy: ref(false),
    toast: () => {},
    fileInputRef: ref(null),
    createExpenseClaimItem: async () => {
      createCount += 1
      return {
        items: [
          { id: 'item-created-1', item_type: 'taxi_receipt', invoice_id: '' }
        ]
      }
    },
    fetchExpenseClaimDetail: async () => claimSnapshots.shift() || { items: [] },
    fetchExpenseClaimItemAttachmentMeta: async () => null,
    fetchExpenseClaimAttachmentAsset: async () => new Blob(['preview']),
    uploadExpenseClaimItemAttachment: async (claimId, itemId, file) => {
      uploadCalls.push({ claimId, itemId, fileName: file.name })
      return {}
    },
    extractReviewAttachmentNames: () => [],
    mergeFilesWithLimit: (existing, incoming) => ({ files: [...existing, ...incoming], overflowCount: 0 }),
    mergeFilePreviews: (existing, incoming) => [...existing, ...incoming],
    isTemporaryPreviewUrl: () => false,
    resolveAttachmentPreviewKind: () => '',
    resolveDocumentPreview: () => null,
    buildFilePreviews: () => [],
    buildFileIdentity: (file) => file.name,
    MAX_ATTACHMENTS: 5,
    VISIBLE_ATTACHMENT_CHIPS: 3,
    clearInlineReviewFieldError: () => {}
  })

  const result = await attachments.syncComposerFilesToDraft('claim-1', [
    { name: 'hotel.pdf' },
    { name: 'taxi.pdf' }
  ])

  assert.deepEqual(uploadCalls, [
    { claimId: 'claim-1', itemId: 'item-empty-1', fileName: 'hotel.pdf' },
    { claimId: 'claim-1', itemId: 'item-created-1', fileName: 'taxi.pdf' }
  ])
  assert.equal(createCount, 1)
  assert.equal(result.uploadedCount, 2)
  assert.equal(result.skippedCount, 0)
})

test('review summary renders markdown and save draft relies on backend response only', () => {
  assert.match(
    createViewTemplateSurface,
    /message\.text && message\.role === 'assistant' && message\.reviewPayload[\s\S]*v-html="ui\.renderMarkdown\(ui\.buildReviewMainMessageText\(message\)\)"/
  )
  assert.doesNotMatch(
    reviewActionsScript,
    /messages\.value\.push\(\s*createMessage\('assistant', actionConfig\.successMessage/
  )
})

test('saved draft review messages stop showing the save-draft prompt', () => {
  const reviewPayload = {
    slot_cards: [
      { key: 'amount', label: '金额', title: '金额', status: 'missing', required: true },
      { key: 'attachments', label: '票据状态', title: '票据状态', status: 'missing', required: true }
    ],
    missing_slots: ['金额', '票据附件'],
    risk_briefs: [],
    confirmation_actions: [
      { label: '保存为草稿', action_type: 'save_draft' }
    ]
  }
  const followup = buildReviewPlainFollowupCopy(reviewPayload, { savedDraft: true })

  assert.equal(followup.lead, '后续处理：')
  assert.match(followup.summary, /自动检测/)
  assert.match(followup.summary, /继续上传/)
  assert.equal(followup.items.length, 0)
  assert.doesNotMatch(followup.summary, /当前草稿待完善|必须/)
  assert.doesNotMatch(followup.summary, /点击|点“草稿”|保存为草稿|临时保存|暂存/)
  assert.match(messageItemTemplate, /buildReviewPlainFollowupForMessage\(message\)/)
  assert.match(createViewScriptSurface, /function isDraftSavedReviewMessage\(message\)/)
  assert.match(createViewScriptSurface, /function canUseInlineSaveDraft\(message\)[\s\S]*isDraftSavedReviewMessage\(message\)/)
})

test('guided save draft emits refresh and exposes reimbursement draft detail card', () => {
  assert.match(
    createViewScriptSurface,
    /emitDraftSaved:\s*\(payload\)\s*=>\s*emit\('draft-saved', payload\)/
  )
  assert.match(submitComposerScript, /function emitSavedDraftRefresh\(draftPayload\)/)
  assert.match(
    submitComposerScript,
    /emitSavedDraftRefresh\(payload\?\.result\?\.draft_payload \|\| null\)/
  )
  assert.match(createViewScriptSurface, /function shouldShowDraftSavedCard\(message\)/)
  assert.match(createViewScriptSurface, /function canOpenDraftDetail\(message\)/)
  assert.match(createViewScriptSurface, /function resolveReimbursementDraftClaimNo\(draftPayload\)/)
  assert.doesNotMatch(createViewScriptSurface, /function buildReimbursementDraftSummaryItems\(draftPayload\)/)
  assert.match(messageItemTemplate, /v-if="ui\.canOpenDraftDetail\(message\)"/)
  assert.match(messageItemTemplate, /class="reimbursement-draft-link"/)
  assert.match(messageItemTemplate, /reimbursement-draft-pending-detail/)
})
