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

import {
  buildLocallySyncedReviewPayload,
  buildReviewNextStepRichCopy,
  buildReviewPlainFollowupCopy,
  isTravelReviewPayload,
  resolveReviewFooterActions
} from '../src/views/scripts/travelReimbursementReviewModel.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 reimbursementService = readFileSync(
  fileURLToPath(new URL('../src/services/reimbursements.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 = readFileSync(
  fileURLToPath(new URL('../src/views/scripts/useTravelReimbursementSubmitComposer.js', import.meta.url)),
  'utf8'
)
const attachmentsScript = readFileSync(
  fileURLToPath(new URL('../src/views/scripts/useTravelReimbursementAttachments.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'
)

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

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

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

test('document review drawer fills sidebar height and preview dialog is centered', () => {
  assert.match(createViewTemplate, /class="insight-body"[\s\S]*:class="\{ 'document-review-body': 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('document preview avoids restored stale object urls', () => {
  assert.match(createViewTemplate, /v-if="documentPreviewDialog\.kind === 'image'"[\s\S]*:key="documentPreviewDialog\.renderKey"/)
  assert.match(createViewTemplate, /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(createViewScript, /shouldShowReviewFactCard\(reviewPayload, 'customer_name'/)
  assert.doesNotMatch(
    createViewScript,
    /key:\s*'customer_name'[\s\S]{0,220}placeholder:\s*'请输入客户名称'[\s\S]{0,80}\},\s*\{[\s\S]{0,80}key:\s*'attachments'/
  )
  assert.match(createViewTemplate, /placeholder="例如：出租车\/网约车票据 \/ 火车\/高铁票"/)
  assert.doesNotMatch(createViewTemplate, /票据场景[\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(createViewTemplate, /class="review-next-step-rich-copy message-answer-markdown"[\s\S]*renderMarkdown\(buildReviewNextStepRichCopyForMessage\(message\)\)/)
  assert.match(createViewTemplate, /class="message-bubble" :class="buildMessageBubbleClass\(message\)"/)
  assert.match(createViewTemplate, /:open="nextStepConfirmDialog\.open"[\s\S]*title="确认提交当前单据？"[\s\S]*confirm-text="确认提交"/)
  assert.match(createViewScript, /const REVIEW_NEXT_STEP_HREF = '#review-next-step'/)
  assert.match(createViewScript, /buildReviewRiskLevelCounts/)
  assert.match(createViewScript, /function buildMessageBubbleClass\(message\)/)
  assert.match(createViewScript, /message-bubble-review-risk-high/)
  assert.match(createViewScript, /message-bubble-review-risk-medium/)
  assert.match(createViewScript, /message-bubble-review-risk-low/)
  assert.match(createViewScript, /function openReviewNextStepConfirm\(message\)/)
  assert.match(createViewScript, /async function confirmReviewNextStepSubmit\(\)/)
  assert.match(createViewScript, /href === REVIEW_NEXT_STEP_HREF[\s\S]*openReviewNextStepConfirm\(message\)/)
  assert.match(createViewScript, /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 = createViewScript.match(/function buildReviewRiskItems\(reviewPayload\) \{[\s\S]*?\n\}\n\nfunction buildReviewRiskConversationText/)
  assert.ok(riskItemsBlock, 'risk item builder should be present')

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

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

  assert.match(
    createViewScript,
    /function appendReviewRiskBriefToConversation\(item\) \{[\s\S]*messages\.value\.push\(createMessage\('assistant'/
  )
  assert.match(createViewScript, /function buildReviewRiskConversationText\(item, detailTarget = \{\}\)/)
  assert.match(createViewScript, /function resolveReviewDetailTarget\(message = null\) \{[\s\S]*router\.resolve\(\{[\s\S]*name: 'app-document-detail'/)
  assert.match(createViewScript, /function resolveReviewRiskDetailTarget\(\) \{[\s\S]*return resolveReviewDetailTarget\(\)/)
  assert.match(createViewScript, /进入 \$\{claimNo\} 详情重新填写/)
  assert.match(createViewTemplate, /class="expense-query-risk-row"[\s\S]*appendExpenseQueryRiskToConversation\(record, risk\)/)
  assert.match(createViewScript, /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(createViewScript, /function normalizeReviewPanelScope\(scope\)/)
  assert.match(createViewScript, /canExposeReviewPanelScope\(item\.reviewPanelScope\)/)
  assert.match(createViewScript, /currentInsight\.value\.intent === 'agent' && agent[\s\S]*return null/)
  assert.match(createViewScript, /function isTravelReviewPayload\(reviewPayload/)
  assert.match(createViewScript, /function resolveReviewTravelTransportType\(reviewPayload/)
  assert.match(createViewScript, /label: '交通类型'[\s\S]*modelKey: 'transport_type'/)
  assert.match(createViewScript, /label: '酒店名称'[\s\S]*modelKey: 'merchant_name'/)
  assert.match(createViewScript, /label: '出差事宜'[\s\S]*editor: 'textarea'[\s\S]*wide: true/)
  assert.match(createViewTemplate, /item\.editor === 'textarea'[\s\S]*<textarea/)
  assert.match(createViewTemplate, /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: resolveReviewPanelScope\(\{/)
  assert.match(submitComposerScript, /nextInsight\.agent\.reviewPanelScope = assistantMessage\.reviewPanelScope/)
})

test('expense query answers keep one clear result structure with document center jump link', () => {
  assert.match(createViewTemplate, /!message\.reviewPayload && !message\.queryPayload && message\.meta\?\.length/)
  assert.match(createViewTemplate, /!message\.reviewPayload && !message\.queryPayload && message\.suggestedActions\?\.length/)
  assert.match(createViewTemplate, /!message\.reviewPayload && !message\.queryPayload && message\.citations\?\.length/)
  assert.match(createViewTemplate, /message\.queryPayload\.title \|\| \(message\.queryPayload\.selectionMode === 'draft_association' \? '选择关联草稿' : '最近 5 条筛选结果'\)/)
  assert.match(createViewTemplate, /v-html="renderMarkdown\(buildExpenseQueryHint\(message\.queryPayload\)\)"/)
  assert.match(createViewScript, /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": f"最近 \{len\(preview_claims\)\} 条\{scope_label\}"/)
})

test('closing the assistant while OCR is running defers unmount until the current flow finishes', () => {
  assert.match(createViewScript, /const closeAfterBusy = ref\(false\)/)
  assert.match(createViewScript, /function isWorkbenchBusy\(\) \{[\s\S]*submitting\.value \|\| reviewActionBusy\.value \|\| sessionSwitchBusy\.value/)
  assert.match(createViewScript, /function maybeFinalizeDeferredClose\(\) \{[\s\S]*!closeAfterBusy\.value \|\| workbenchVisible\.value \|\| isWorkbenchBusy\(\)/)
  assert.match(createViewScript, /function requestCloseWorkbench\(\) \{[\s\S]*closeAfterBusy\.value = isWorkbenchBusy\(\)[\s\S]*workbenchVisible\.value = false/)
  assert.match(createViewScript, /function emitCloseAfterLeave\(\) \{[\s\S]*closeAfterBusy\.value && isWorkbenchBusy\(\)[\s\S]*return/)
  assert.match(createViewScript, /\[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(createViewTemplate, /v-if="canShowTravelCalculator" class="travel-calculator-anchor"/)
  assert.match(createViewTemplate, /class="tool-btn composer-side-btn travel-calculator-trigger"[\s\S]*差旅计算器/)
  assert.match(createViewTemplate, /class="travel-calculator-popover"[\s\S]*v-model="travelCalculatorForm\.days"[\s\S]*v-model="travelCalculatorForm\.location"/)
  assert.doesNotMatch(createViewTemplate, /travel-calculator-modal/)
  assert.doesNotMatch(createViewTemplate, /travelCalculatorResult\.total_amount/)
  assert.match(createViewScript, /calculateTravelReimbursement/)
  assert.match(createViewScript, /const canShowTravelCalculator = computed\(\(\) => activeSessionType\.value === SESSION_TYPE_EXPENSE\)/)
  assert.match(createViewScript, /function openTravelCalculator\(\) \{[\s\S]*!canShowTravelCalculator\.value[\s\S]*closeTravelCalculator\(\)/)
  assert.match(createViewScript, /function toggleTravelCalculator\(\)/)
  assert.match(createViewScript, /function toggleTravelCalculator\(\) \{[\s\S]*!canShowTravelCalculator\.value[\s\S]*closeTravelCalculator\(\)/)
  assert.match(createViewScript, /watch\(canShowTravelCalculator,[\s\S]*closeTravelCalculator\(\)/)
  assert.match(createViewScript, /function submitTravelCalculator\(\) \{[\s\S]*calculateTravelReimbursement\(\{[\s\S]*grade: String\(user\.grade/)
  assert.match(createViewScript, /根据您输入的地点和天数/)
  assert.match(createViewScript, /匹配到您要出差的地区为/)
  assert.match(createViewScript, /参考可报销合计/)
  assert.match(createViewScript, /住宿费：\$\{hotelRate\} × \$\{days\} = \$\{hotelAmount\} 元/)
  assert.match(createViewScript, /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(createViewScript, /function buildReviewFormContextFromPayload\(reviewPayload, inlineState = null\)/)
  assert.match(createViewScript, /function buildBusinessTimeContextFromReviewValues\(values = \{\}\)/)
  assert.match(
    createViewScript,
    /resolvedUploadDisposition === 'continue_existing'[\s\S]*buildReviewFormContextFromPayload\([\s\S]*activeReviewPayload\.value[\s\S]*reviewInlineForm\.value[\s\S]*extraContext\.review_form_values/s
  )
  assert.match(
    createViewScript,
    /inheritedReviewContext\.business_time_context[\s\S]*extraContext\.business_time_context = inheritedReviewContext\.business_time_context/s
  )
})

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

test('draft creation starts composer attachment persistence after response rendering', () => {
  assert.match(
    submitComposerScript,
    /void syncComposerFilesToDraft\(resolvedDraftClaimId, files\)\s*\.then\(\(\) => \{\s*persistSessionState\(\)\s*\}\)\s*\.catch\(\(error\) => \{/s
  )
  assert.doesNotMatch(
    submitComposerScript,
    /await syncComposerFilesToDraft\(resolvedDraftClaimId, files\)/
  )
  assert.ok(
    submitComposerScript.indexOf('replaceMessage(pendingMessage.id, assistantMessage)') <
      submitComposerScript.indexOf('void syncComposerFilesToDraft(resolvedDraftClaimId, files)'),
    'assistant response should render before background attachment persistence starts'
  )
  assert.match(attachmentsScript, /function normalizeAttachmentMatchName\(value\)/)
  assert.match(attachmentsScript, /const normalizedMatchBuckets = new Map\(\)/)
  assert.match(
    attachmentsScript,
    /const targetItem = nextExactMatch \|\| nextNormalizedMatch \|\| fallbackMatch/
  )
})

test('review summary renders markdown and save draft relies on backend response only', () => {
  assert.match(
    createViewTemplate,
    /message\.text && message\.role === 'assistant' && message\.reviewPayload[\s\S]*v-html="renderMarkdown\(message\.text\)"/
  )
  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.doesNotMatch(followup.summary, /点击|点“草稿”|保存为草稿|临时保存|暂存/)
  assert.match(createViewTemplate, /buildReviewPlainFollowupForMessage\(message\)/)
  assert.match(createViewScript, /function isDraftSavedReviewMessage\(message\)/)
  assert.match(createViewScript, /function canUseInlineSaveDraft\(message\)[\s\S]*isDraftSavedReviewMessage\(message\)/)
})
