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

import {
  buildAiAdviceViewModel,
  buildAttachmentInsightViewModel,
  buildAttachmentRiskCards,
  buildClaimSummaryRiskCards,
  buildItemClaimRiskState,
  extractRiskTagsFromText,
  resolveRiskTags,
  resolveRiskTagTone
} from '../src/views/scripts/travelRequestDetailInsights.js'
import {
  buildExpenseItemViewModel,
  buildDraftBlockingIssues,
  buildOptionalTravelReceiptRiskCards,
  isApplicationDocumentRequest
} from '../src/views/scripts/travelRequestDetailExpenseModel.js'

const detailViewTemplate = readFileSync(
  fileURLToPath(new URL('../src/views/TravelRequestDetailView.vue', import.meta.url)),
  'utf8'
)
const detailViewScript = readFileSync(
  fileURLToPath(new URL('../src/views/scripts/TravelRequestDetailView.js', import.meta.url)),
  'utf8'
)
const detailViewStyle = readFileSync(
  fileURLToPath(new URL('../src/assets/styles/views/travel-request-detail-view.css', import.meta.url)),
  'utf8'
)
const requestsComposableScript = readFileSync(
  fileURLToPath(new URL('../src/composables/useRequests.js', import.meta.url)),
  'utf8'
)
const approvalCenterTemplate = readFileSync(
  fileURLToPath(new URL('../src/views/ApprovalCenterView.vue', import.meta.url)),
  'utf8'
)
const approvalCenterScript = readFileSync(
  fileURLToPath(new URL('../src/views/scripts/ApprovalCenterView.js', import.meta.url)),
  'utf8'
)
const returnReasonDialog = readFileSync(
  fileURLToPath(new URL('../src/components/shared/ReturnReasonDialog.vue', import.meta.url)),
  'utf8'
)

const attachmentMeta = {
  file_name: 'taxi-invoice.pdf',
  media_type: 'application/pdf',
  previewable: true,
  document_info: {
    document_type: 'taxi_receipt',
    document_type_label: '出租车/网约车票据',
    fields: [
      { label: '金额', value: '121.54' },
      { label: '日期', value: '2026-03-04' }
    ]
  },
  requirement_check: {
    matches: false,
    message: '附件类型与当前费用项目不匹配。'
  },
  analysis: {
    severity: 'high',
    label: '高风险',
    headline: '票据类型不匹配',
    summary: '交通票据挂在办公用品费明细下。',
    points: ['票据识别为出租车/网约车票据', '当前费用项目为办公用品费'],
    suggestion: '把费用项目调整为交通费，或更换为办公用品票据。'
  }
}

test('attachment insight exposes recognition fields and rule basis', () => {
  const insight = buildAttachmentInsightViewModel(attachmentMeta, {
    name: '办公用品费',
    itemType: 'office'
  })

  assert.equal(insight.documentTypeLabel, '出租车/网约车票据')
  assert.equal(insight.requirementLabel, '不符合当前费用类型')
  assert.deepEqual(insight.fields, ['金额：121.54', '日期：2026-03-04'])
  assert.ok(insight.ruleBasis.some((item) => item.includes('附件类型与当前费用项目不匹配')))
})

test('AI advice card splits every attachment risk point with basis and suggestion', () => {
  const riskCards = buildAttachmentRiskCards({
    expenseItems: [
      {
        id: 'item-1',
        name: '办公用品费',
        invoiceId: 'taxi-invoice.pdf'
      }
    ],
    attachmentMetaByItemId: {
      'item-1': attachmentMeta
    }
  })
  const advice = buildAiAdviceViewModel({
    completionItems: [],
    riskCards
  })

  assert.equal(riskCards.length, 2)
  assert.equal(advice.badge, '优先整改')
  assert.equal(advice.riskCards.length, 2)
  assert.ok(advice.riskCards.every((card) => card.ruleBasis.length > 0))
  assert.ok(advice.riskCards.every((card) => card.suggestion.includes('费用项目调整为交通费')))
})

test('risk cards carry severity and domain tags for statistics', () => {
  const hotelRisk = {
    tone: 'high',
    title: '住宿超标待说明',
    risk: '住宿标准：北京酒店 800 元/晚超出报销标准。'
  }
  const trafficRisk = {
    tone: 'medium',
    title: '交通票据提醒',
    risk: '火车票说明格式待调整。'
  }

  assert.deepEqual(resolveRiskTags(hotelRisk), ['#high_risk', '#hotel'])
  assert.deepEqual(resolveRiskTags(trafficRisk), ['#middle_risk', '#traffic'])
  assert.equal(resolveRiskTagTone('#hotel'), 'hotel')
  assert.deepEqual(extractRiskTagsFromText('超标说明：#high_risk #hotel 原因'), ['#high_risk', '#hotel'])
})

test('AI advice splits claim attachment risk flags into specific points', () => {
  const riskCards = buildAttachmentRiskCards({
    claimRiskFlags: [
      {
        source: 'attachment_analysis',
        severity: 'medium',
        label: '中风险',
        message: '费用明细第 2 条：日期字段：未识别到开票日期。',
        summary: '当前附件可见部分内容，但金额、用途、日期或附件类型仍有缺失或不一致。',
        points: [
          '日期字段：未识别到开票日期或业务发生日期。',
          '金额字段：附件识别金额 300.00 元与报销金额 88.00 元不一致。'
        ]
      }
    ]
  })

  assert.equal(riskCards.length, 2)
  assert.equal(riskCards[0].risk, '日期字段：未识别到开票日期或业务发生日期。')
  assert.equal(riskCards[1].risk, '金额字段：附件识别金额 300.00 元与报销金额 88.00 元不一致。')
  assert.ok(riskCards[0].ruleBasis.some((item) => item.includes('风险汇总')))
})

test('AI advice keeps visible risk flags when backend uses tone instead of severity', () => {
  const riskCards = buildAttachmentRiskCards({
    claimRiskFlags: [
      {
        source: 'submission_review',
        tone: 'medium',
        label: '中风险',
        message: '直属领导缺失，当前单据需审批环节补充分配。'
      }
    ]
  })

  assert.equal(riskCards.length, 1)
  assert.equal(riskCards[0].tone, 'medium')
  assert.equal(riskCards[0].risk, '直属领导缺失，当前单据需审批环节补充分配。')
  assert.ok(riskCards[0].ruleBasis.some((item) => item.includes('审批链校验')))
  assert.ok(riskCards[0].suggestion.includes('员工档案'))
})

test('AI advice falls back to claim risk summary instead of showing an empty risk area', () => {
  const riskCards = buildClaimSummaryRiskCards({
    riskSummary: 'AI预审发现 1 条中风险附件，已随单流转给审批人复核。'
  })

  assert.equal(riskCards.length, 1)
  assert.equal(riskCards[0].tone, 'medium')
  assert.equal(riskCards[0].label, '中风险')
  assert.match(riskCards[0].risk, /中风险附件/)
  assert.ok(riskCards[0].ruleBasis.some((item) => item.includes('风险汇总')))
  assert.ok(riskCards[0].suggestion.includes('附件预览'))
})

test('AI advice ignores approval opinions and flow logs as risks', () => {
  const riskCards = buildAttachmentRiskCards({
    claimRiskFlags: [
      {
        source: 'manual_approval',
        severity: 'info',
        label: '领导审批通过',
        message: '同意'
      },
      {
        source: 'finance_approval',
        severity: 'info',
        label: '财务审核通过',
        message: '周晓彤 已完成财务审核，进入归档入账。'
      }
    ]
  })

  assert.deepEqual(riskCards, [])
  assert.deepEqual(buildClaimSummaryRiskCards({ riskSummary: '同意' }), [])
  assert.deepEqual(buildClaimSummaryRiskCards({ riskSummary: '周晓彤 已完成财务审核，进入归档入账。' }), [])
})

test('expense row risk state falls back to claim item risk flags', () => {
  const state = buildItemClaimRiskState(
    {
      id: 'hotel-item',
      name: '住宿费'
    },
    [
      {
        source: 'attachment_analysis',
        item_id: 'hotel-item',
        severity: 'high',
        label: '高风险',
        message: '费用明细第 2 条：住宿标准：当前酒店识别金额约 880.00 元/晚。',
        summary: '当前住宿票据金额超过规则中心差旅住宿标准。',
        points: ['住宿标准：当前酒店识别金额约 880.00 元/晚。']
      }
    ]
  )

  assert.equal(state.tone, 'high')
  assert.equal(state.label, '高风险')
  assert.match(state.summary, /住宿票据金额超过/)
  assert.deepEqual(state.points, ['住宿标准：当前酒店识别金额约 880.00 元/晚。'])
})

test('attachment risk cards do not duplicate claim fallback flags for the same item', () => {
  const riskCards = buildAttachmentRiskCards({
    expenseItems: [
      {
        id: 'hotel-item',
        name: '住宿费',
        invoiceId: 'hotel-risk.png'
      }
    ],
    attachmentMetaByItemId: {
      'hotel-item': {
        analysis: {
          severity: 'high',
          label: '高风险',
          headline: 'AI提示：住宿金额超出报销标准',
          summary: '当前住宿票据金额超过规则中心差旅住宿标准。',
          points: ['住宿标准：当前酒店识别金额约 880.00 元/晚。'],
          suggestion: '请补充超标说明。'
        }
      }
    },
    claimRiskFlags: [
      {
        source: 'attachment_analysis',
        item_id: 'hotel-item',
        severity: 'high',
        label: '高风险',
        message: '费用明细第 1 条：住宿标准：当前酒店识别金额约 880.00 元/晚。',
        summary: '当前住宿票据金额超过规则中心差旅住宿标准。',
        points: ['住宿标准：当前酒店识别金额约 880.00 元/晚。']
      }
    ]
  })

  assert.equal(riskCards.length, 1)
  assert.equal(riskCards[0].risk, '住宿标准：当前酒店识别金额约 880.00 元/晚。')
})

test('AI advice view model exposes grouped completion and risk sections', () => {
  const advice = buildAiAdviceViewModel({
    completionItems: ['补充业务地点', '补充报销金额'],
    riskCards: [
      {
        id: 'risk-1',
        tone: 'high',
        label: '高风险',
        title: '票据类型不匹配',
        risk: '交通票据挂在办公用品费明细下。',
        ruleBasis: ['附件类型与当前费用项目不匹配。'],
        suggestion: '把费用项目调整为交通费。'
      }
    ]
  })

  assert.equal(advice.sections.length, 2)
  assert.deepEqual(
    advice.sections.map((section) => ({ title: section.title, kind: section.kind })),
    [
      { title: '建议补充字段', kind: 'completion' },
      { title: '已知存在风险', kind: 'risk' }
    ]
  )
  assert.deepEqual(advice.sections[0].items, ['补充业务地点', '补充报销金额'])
  assert.equal(advice.sections[1].items.length, 1)
})

test('AI advice view model omits empty sections', () => {
  const readyAdvice = buildAiAdviceViewModel({
    completionItems: [],
    riskCards: []
  })
  const completionOnlyAdvice = buildAiAdviceViewModel({
    completionItems: ['补充业务地点'],
    riskCards: []
  })
  const riskOnlyAdvice = buildAiAdviceViewModel({
    completionItems: [],
    riskCards: [
      {
        id: 'risk-1',
        tone: 'medium',
        label: '中风险',
        title: '说明不完整',
        risk: '缺少业务背景。',
        ruleBasis: ['系统预审规则命中该风险提示。'],
        suggestion: '补充说明。'
      }
    ]
  })

  assert.deepEqual(readyAdvice.sections, [])
  assert.equal(readyAdvice.badge, '可直接提交')
  assert.deepEqual(completionOnlyAdvice.sections.map((section) => section.title), ['建议补充字段'])
  assert.deepEqual(riskOnlyAdvice.sections.map((section) => section.title), ['已知存在风险'])
})

test('AI advice template renders grouped section titles with completion before risk', () => {
  assert.match(detailViewTemplate, /v-if="showAiAdvicePanel" class="detail-card panel validation-card"/)
  assert.match(detailViewTemplate, /<h3>\{\{ aiAdviceTitle \}\}<\/h3>/)
  assert.match(detailViewTemplate, /<p>\{\{ aiAdviceHint \}\}<\/p>/)
  assert.match(detailViewScript, /buildClaimSummaryRiskCards\(request\.value\)/)
  assert.match(detailViewScript, /const showAiAdvicePanel = computed\(\(\) => isEditableRequest\.value \|\| aiAdvice\.value\.riskCards\.length > 0\)/)
  assert.match(detailViewTemplate, /v-if="aiAdvice\.sections\.length" class="validation-sections"/)
  assert.match(detailViewTemplate, /v-for="section in aiAdvice\.sections"/)
  assert.match(detailViewTemplate, /validation-section--\$\{section\.kind\}/)
  assert.match(detailViewTemplate, /<h4 class="validation-section-title">\{\{ section\.title \}\}<\/h4>/)
  assert.match(detailViewTemplate, /v-if="section\.kind === 'completion'" class="validation-list"/)
  assert.match(detailViewTemplate, /v-else class="risk-advice-list"/)
  assert.ok(
    detailViewTemplate.indexOf("section.kind === 'completion'") < detailViewTemplate.indexOf('risk-advice-card')
  )
})

test('AI advice risk section uses compact card styling hooks', () => {
  assert.match(detailViewTemplate, /class="\['risk-advice-card', card\.tone\]"/)
  assert.doesNotMatch(detailViewTemplate, /card\.tags\?\.length/)
  assert.doesNotMatch(detailViewTemplate, /risk-card-tag-list/)
  assert.doesNotMatch(detailViewTemplate, /risk-note-tag/)
  assert.match(detailViewScript, /tags: resolveRiskTags\(card\)/)
  assert.match(detailViewStyle, /\.validation-card \{\s*border: 1px solid #e5e7eb;/)
  assert.match(detailViewStyle, /\.validation-section--risk \.risk-advice-card \{\s*display: grid;\s*gap: 8px;\s*padding: 12px 12px 11px;/)
  assert.match(detailViewStyle, /\.validation-section--risk \.risk-advice-card\.low/)
  assert.match(detailViewStyle, /\.risk-advice-card\.low/)
  assert.doesNotMatch(detailViewStyle, /\.risk-note-tag/)
  assert.match(detailViewStyle, /\.validation-section--risk \.risk-advice-meta ul,\s*\.validation-section--risk \.risk-advice-meta p \{\s*margin: 0;/)
})

test('expense rows show a major-risk warning icon before time', () => {
  assert.match(detailViewTemplate, /'has-major-risk': isMajorExpenseRisk\(item\)/)
  assert.match(detailViewTemplate, /class="mdi mdi-alert expense-risk-indicator"/)
  assert.match(detailViewStyle, /\.expense-risk-indicator \{/)
  assert.match(detailViewScript, /function isMajorExpenseRisk\(item\)/)
  assert.match(detailViewScript, /buildItemClaimRiskState\(item, resolveClaimRiskFlags\(\)\)/)
})

test('AI advice shows only the latest manual return while preserving return count context', () => {
  const riskCards = buildAttachmentRiskCards({
    claimRiskFlags: [
      {
        source: 'manual_return',
        severity: 'medium',
        label: '人工退回',
        message: '第一次退回：缺少附件。',
        reason: '缺少附件。',
        return_count: 1,
        return_stage: '直属领导审批',
        risk_points: ['附件缺失或不清晰']
      },
      {
        source: 'manual_return',
        severity: 'medium',
        label: '人工退回',
        message: '第二次退回：超标说明不完整。',
        reason: '超标说明不完整。',
        return_count: 2,
        return_stage: '财务审批',
        risk_points: ['超出制度标准或缺少超标说明']
      }
    ]
  })

  assert.equal(riskCards.length, 1)
  assert.equal(riskCards[0].risk, '第二次退回：超标说明不完整。')
  assert.ok(riskCards[0].ruleBasis.some((item) => item.includes('累计退回 2 次')))
  assert.ok(riskCards[0].ruleBasis.some((item) => item.includes('财务审批')))
})

test('expense attachment actions keep preview as the only recognition entry point', () => {
  assert.match(detailViewTemplate, /:aria-label="resolveAttachmentPreviewTitle\(item\)"/)
  assert.match(detailViewScript, /return fileName \? `预览附件：\$\{fileName\}` : '预览附件'/)
  assert.match(detailViewScript, /\.filter\(\(item\) => canPreviewAttachment\(item\)\)/)
  assert.match(detailViewScript, /function hasStoredAttachmentReference\(item\) \{[\s\S]*return String\(item\?\.invoiceId \|\| ''\)\.includes\('\/'\)/)
  assert.match(detailViewScript, /if \(metadata\) \{[\s\S]*return metadata\.previewable !== false[\s\S]*return true/)
  assert.match(detailViewScript, /原件尚未保存到单据中，请重新上传后预览/)
  assert.doesNotMatch(detailViewTemplate, /aria-label="识别附件"/)
  assert.doesNotMatch(detailViewTemplate, /点击识别按钮/)
  assert.doesNotMatch(detailViewScript, /recognizeExpenseAttachment/)
  assert.doesNotMatch(detailViewScript, /recognizingExpenseId/)
})

test('expense detail table shows the amount total below detail rows', () => {
  assert.match(detailViewTemplate, /<div[^>]*class="detail-expense-table"/)
  assert.match(detailViewTemplate, /当前还没有费用明细/)
  assert.doesNotMatch(detailViewTemplate, /class="total-row"/)
  assert.match(detailViewTemplate, /class="expense-total-under-table"[\s\S]*金额合计[\s\S]*\{\{ expenseTotal \}\}/)
  assert.doesNotMatch(detailViewTemplate, /\{\{ uploadedExpenseCount \}\} 项已关联票据/)
  assert.doesNotMatch(detailViewTemplate, /\{\{ expenseSummaryText \}\}/)
})

test('additional note is shown above expense details as travel purpose text', () => {
  assert.ok(
    detailViewTemplate.indexOf('<h3>附加说明</h3>')
      < detailViewTemplate.indexOf("isApplicationDocument ? '申请详情' : '费用明细'")
  )
  assert.match(detailViewTemplate, /<article v-if="!isApplicationDocument" class="detail-card panel">/)
  assert.match(detailViewTemplate, /用于说明本次出差或办事目的/)
  assert.match(detailViewTemplate, /v-if="canEditDetailNote" class="detail-note-editor"/)
  assert.match(detailViewTemplate, /v-else class="detail-note readonly"/)
  assert.match(detailViewTemplate, /v-model="detailNoteEditorView"/)
  assert.match(detailViewTemplate, /提交后将作为明确说明展示/)
  assert.match(detailViewScript, /const canEditDetailNote = computed\(\(\) => isDraftRequest\.value\)/)
  assert.match(detailViewScript, /function normalizeDetailNoteDraftValue\(value\)/)
  assert.match(detailViewScript, /function stripRiskTagsForDisplay\(value\)/)
  assert.match(detailViewScript, /function mergeVisibleNoteWithHiddenTags\(visibleText, rawText\)/)
  assert.match(detailViewScript, /const detailNoteSource = computed\(\(\) => normalizeDetailNoteDraftValue\(request\.value\.note\)\)/)
  assert.match(detailViewScript, /updateExpenseClaim\(request\.value\.claimId/)
  assert.match(detailViewScript, /emit\('request-updated', \{ claimId: request\.value\.claimId \}\)/)
  assert.match(detailViewScript, /暂无附加说明。请补充本次出差或办事事由/)
  assert.match(detailViewScript, /去北京客户现场出差，拜访 XX 客户并处理项目验收事项/)
  assert.match(detailViewStyle, /\.detail-note-editor textarea/)
  assert.match(detailViewStyle, /\.detail-note\.readonly/)
})

test('ticket item types and system allowance row are visible but read only', () => {
  assert.match(detailViewScript, /value: 'train_ticket', label: '火车票'/)
  assert.match(detailViewScript, /value: 'flight_ticket', label: '机票'/)
  assert.match(detailViewScript, /value: 'hotel_ticket', label: '住宿票'/)
  assert.match(detailViewScript, /value: 'ride_ticket', label: '乘车'/)
  assert.match(detailViewScript, /value: 'travel_allowance', label: '出差补贴'/)
  assert.match(detailViewScript, /const SYSTEM_GENERATED_EXPENSE_TYPES = new Set\(\['travel_allowance'\]\)/)
  assert.match(detailViewTemplate, /'system-generated-row': item\.isSystemGenerated/)
  assert.match(detailViewTemplate, /v-if="item\.isSystemGenerated" class="system-row-lock"/)
  assert.match(detailViewTemplate, /v-if="item\.isSystemGenerated" class="system-attachment-note"/)
  assert.match(detailViewScript, /系统自动计算的补贴行不能手动编辑/)
  assert.match(detailViewScript, /系统自动计算的补贴行不能删除/)
})

test('travel item date caption distinguishes departure return and trip events', () => {
  assert.match(detailViewTemplate, /<span>\{\{ item\.dayLabel \}\}<\/span>/)
  assert.match(detailViewScript, /const LONG_DISTANCE_TRAVEL_EXPENSE_TYPES = new Set\(\['train_ticket', 'flight_ticket'\]\)/)
  assert.match(detailViewScript, /function buildTravelTimeLabelMap\(items, requestModel\)/)
  assert.match(detailViewScript, /labels\.set\(item\.id, '出发时间'\)/)
  assert.match(detailViewScript, /labels\.set\(item\.id, '返回时间'\)/)
  assert.match(detailViewScript, /return '乘车时间'/)
  assert.match(detailViewScript, /return '住宿时间'/)
  assert.match(requestsComposableScript, /function buildTravelTimeLabelMap\(items, claim\)/)
  assert.match(requestsComposableScript, /return claim\?\.expense_type === 'travel' \? '出行时间' : '业务发生时间'/)
  assert.doesNotMatch(detailViewScript, /第 \$\{index \+ 1\} 项/)
})

test('expense detail table shows each item filled time from item creation time', () => {
  assert.match(detailViewTemplate, /<th class="col-filled-at">填写时间<\/th>[\s\S]*<th class="col-time">发生时间<\/th>/)
  assert.match(detailViewTemplate, /<td class="expense-filled-at col-filled-at">[\s\S]*\{\{ item\.filledAt \}\}[\s\S]*<td :class="\['expense-time col-time'/)
  assert.match(detailViewTemplate, /<span>条款填写时间<\/span>/)
  assert.match(detailViewScript, /function formatExpenseFilledTime\(value\)/)
  assert.match(detailViewScript, /source\?\.filledAt[\s\S]*source\?\.created_at/)
  assert.match(detailViewScript, /expenseTableColumnCount = computed\(\s*\(\) => 6 \+ \(isEditableRequest\.value \? 1 : 0\)/)
  assert.match(requestsComposableScript, /filledAt: formatDateTime\(item\?\.created_at\) \|\| '待同步'/)
})

test('expense item upload remains limited to one receipt per detail row', () => {
  assert.match(detailViewTemplate, /ref="expenseUploadInput"[\s\S]*type="file"/)
  assert.doesNotMatch(detailViewTemplate, /\bmultiple\b/)
  assert.equal(
    (detailViewTemplate.match(/v-if="isEditableRequest && !item\.invoiceId && !item\.isSystemGenerated"/g) || []).length,
    2
  )
  assert.match(detailViewScript, /const attachments = invoiceId \? \[attachmentName \|\| invoiceId\] : \[\]/)
  assert.match(detailViewScript, /attachmentStatus: isSystemGenerated \? '无需附件' : attachments\.length \? '已关联票据' : '未上传'/)
  assert.match(detailViewScript, /if \(item\?\.invoiceId\) \{[\s\S]*每条费用明细只能关联一张单据/)
  assert.match(detailViewScript, /const fileCount = fileList\?\.length \|\| 0/)
  assert.match(detailViewScript, /fileCount > 1[\s\S]*一条费用明细只能上传一张单据/)
})

test('expense item upload patches OCR amount into the visible detail row', () => {
  assert.match(detailViewScript, /const recognizedItemAmount = Number\(payload\?\.item_amount \?\? payload\?\.itemAmount\)/)
  assert.match(detailViewScript, /const recognizedItemDate = normalizeIsoDateValue\(payload\?\.item_date \?\? payload\?\.itemDate\)/)
  assert.match(detailViewScript, /const recognizedItemType = String\(payload\?\.item_type \?\? payload\?\.itemType \?\? ''\)\.trim\(\)/)
  assert.match(detailViewScript, /const recognizedItemReason = String\(payload\?\.item_reason \?\? payload\?\.itemReason \?\? ''\)\.trim\(\)/)
  assert.match(detailViewScript, /itemPatch\.itemAmount = recognizedItemAmount/)
  assert.match(detailViewScript, /itemPatch\.amount = formatCurrency\(recognizedItemAmount\)/)
  assert.match(detailViewScript, /populateExpenseEditor\(\{ \.\.\.item, \.\.\.itemPatch \}\)/)
})

test('expense detail edit keeps delete but removes cancel and allows draft placeholders', () => {
  assert.doesNotMatch(detailViewTemplate, /@click="cancelExpenseEdit"/)
  assert.doesNotMatch(detailViewScript, /function cancelExpenseEdit/)
  assert.match(detailViewScript, /if \(expenseEditor\.itemDate && !isValidIsoDate\(expenseEditor\.itemDate\)\)/)
  assert.doesNotMatch(detailViewScript, /请输入费用说明。/)
  assert.doesNotMatch(detailViewScript, /请输入大于 0 的费用金额。/)
  assert.match(detailViewScript, /const amountText = String\(expenseEditor\.itemAmount \|\| ''\)\.trim\(\)/)
  assert.match(detailViewScript, /const nextAmount = amountText \? Number\(amountText\) : 0/)
  assert.match(detailViewScript, /if \(expenseEditor\.itemDate\) \{[\s\S]*itemPayload\.item_date = expenseEditor\.itemDate/)
})

test('travel detail AI advice adds low risk reminders for optional receipts', () => {
  assert.match(detailViewScript, /function buildOptionalTravelReceiptRiskCards\(requestModel, items\)/)
  assert.match(detailViewScript, /isApplicationDocumentRequest\(requestModel\)[\s\S]*return \[\]/)
  assert.match(detailViewScript, /id: 'travel-optional-hotel-ticket'[\s\S]*tone: 'low'[\s\S]*住宿票据提醒/)
  assert.match(detailViewScript, /不要忘记补充酒店住宿票据/)
  assert.match(detailViewScript, /id: 'travel-optional-ride-ticket'[\s\S]*tone: 'low'[\s\S]*乘车票据提醒/)
  assert.match(detailViewScript, /可以继续补充票据报销/)
  assert.match(
    detailViewScript,
    /\.\.\.buildOptionalTravelReceiptRiskCards\(request\.value, expenseItems\.value\)/
  )
})

test('expense detail save is blocked while attachment recognition is running', () => {
  assert.match(detailViewScript, /const uploadingExpenseId = ref\(''\)/)
  assert.match(detailViewScript, /const actionBusy = computed\(\(\) =>[\s\S]*Boolean\(uploadingExpenseId\.value\)/)
  assert.match(detailViewScript, /const canSubmit = computed\(\(\) => isEditableRequest\.value && !actionBusy\.value\)/)
  assert.match(detailViewScript, /if \(draftBlockingIssues\.value\.length\) \{[\s\S]*请先补全草稿信息，再提交审批。/)
  assert.match(
    detailViewTemplate,
    /@click="saveExpenseEdit\(item\)"[\s\S]*:disabled="actionBusy"/
  )
  assert.match(
    detailViewScript,
    /if \(actionBusy\.value\) \{[\s\S]*toast\(uploadingExpenseId\.value \? '附件识别中，请等待识别完成后再保存。' : '当前操作处理中，请稍后再保存。'\)[\s\S]*return/
  )
})

test('application detail uses application labels instead of reimbursement labels', () => {
  assert.match(detailViewTemplate, /isApplicationDocument \? '申请进度'/)
  assert.match(detailViewTemplate, /isApplicationDocument \? '申请详情' : '费用明细'/)
  assert.match(detailViewTemplate, /展示本次申请的事实信息、职级规则测算和用户预估费用/)
  assert.match(detailViewTemplate, /class="application-detail-facts"/)
  assert.match(detailViewTemplate, /applicationDetailFactItems/)
  assert.match(detailViewScript, /buildApplicationDetailFactItems/)
  assert.match(detailViewStyle, /\.application-detail-fact\.highlight strong/)
  assert.match(detailViewTemplate, /isApplicationDocument \? '申请类型' : '报销类型'/)
  assert.match(detailViewTemplate, /isApplicationDocument \? '预计金额' : '报销金额'/)
  assert.match(detailViewTemplate, /isApplicationDocument \? '退回申请' : '退回单据'/)
  assert.match(detailViewTemplate, /当前申请单已进入流程，详情页仅展示状态与申请信息。/)
})

test('application detail does not show optional travel receipt reminders', () => {
  const request = {
    documentTypeCode: 'application',
    claimNo: 'APP-20260525-ABC123',
    typeCode: 'travel_application',
    detailVariant: 'travel'
  }

  assert.equal(isApplicationDocumentRequest(request), true)
  assert.deepEqual(
    buildOptionalTravelReceiptRiskCards(request, [
      { id: 'allowance', itemType: 'travel_allowance', isSystemGenerated: true, invoiceId: '' }
    ]),
    []
  )
})

test('draft submit validation uses expense detail date and amount when claim summary is stale', () => {
  const issues = buildDraftBlockingIssues(
    {
      profileName: '张三',
      typeLabel: '待补充',
      typeCode: 'office',
      reason: '待补充',
      location: '待补充',
      occurredDisplay: '待补充',
      amountValue: 0
    },
    [
      {
        id: 'item-1',
        itemDate: '2026-05-21',
        itemType: 'office',
        itemReason: '采购办公用品',
        itemLocation: '',
        itemAmount: 88,
        invoiceId: 'claim-1/item-1/office-note.png'
      }
    ]
  )

  assert.ok(!issues.some((issue) => issue.includes('发生时间未完善')))
  assert.ok(!issues.some((issue) => issue.includes('报销金额未完善')))
  assert.ok(!issues.some((issue) => issue.includes('报销类型未完善')))
  assert.ok(!issues.some((issue) => issue.includes('报销事由未完善')))
})

test('transport ticket descriptions use route format and invalid format becomes risk advice', () => {
  const routeItem = buildExpenseItemViewModel(
    {
      id: 'route-item',
      itemType: 'train_ticket',
      itemReason: '广州南-上海虹桥',
      itemLocation: '上海',
      itemAmount: 354,
      invoiceId: 'train-ticket.png'
    },
    0,
    { claimId: 'claim-route', detailVariant: 'travel' }
  )
  const shipItem = buildExpenseItemViewModel(
    {
      id: 'ship-item',
      itemType: 'ship_ticket',
      itemReason: '上海港-舟山港',
      itemLocation: '舟山',
      itemAmount: 120,
      invoiceId: 'ship-ticket.png'
    },
    1,
    { claimId: 'claim-route', detailVariant: 'travel' }
  )

  assert.equal(routeItem.desc, '广州南-上海虹桥')
  assert.equal(routeItem.detail, '起始地-目的地')
  assert.equal(shipItem.name, '轮船票')
  assert.equal(shipItem.detail, '起始地-目的地')
  assert.match(detailViewScript, /const ROUTE_DESCRIPTION_EXPENSE_TYPES = new Set\(\['train_ticket', 'flight_ticket', 'ship_ticket', 'ferry_ticket', 'ride_ticket'\]\)/)
  assert.match(detailViewScript, /const HOTEL_DESCRIPTION_EXPENSE_TYPES = new Set\(\['hotel_ticket'\]\)/)
  assert.match(detailViewScript, /const ROUTE_DESCRIPTION_PATTERN = \/\^\[A-Za-z0-9\\u4e00-\\u9fa5/)
  assert.match(detailViewScript, /return '起始地-目的地，例如：广州南-北京南'/)
  assert.match(detailViewScript, /return '起始地-目的地'/)
  assert.match(detailViewScript, /return '目的地酒店，例如：北京中心酒店'/)
  assert.match(detailViewScript, /return '目的地酒店'/)
  assert.match(detailViewScript, /isSyntheticLocationDisplay\(item\.detail, item\.itemType\)/)
  assert.match(
    detailViewScript,
    /isRouteDescriptionExpenseType\(item\.itemType\) && !isValidRouteDescription\(item\.itemReason\)[\s\S]*issues\.push\('行程说明格式错误'\)/
  )
  assert.match(
    detailViewScript,
    /isRouteDescriptionExpenseType\(expenseEditor\.itemType\)[\s\S]*!isValidRouteDescription\(expenseEditor\.itemReason\)[\s\S]*return '行程说明格式应为“起始地-目的地”，例如：广州南-北京南。'/
  )
  assert.match(
    detailViewScript,
    /fieldText === '行程说明格式错误'[\s\S]*return `\$\{labelPrefix\}的行程说明，格式应为“起始地-目的地”。`/
  )
})

test('transport ticket items no longer generate business location completion advice', () => {
  const locationRequiredBlock = detailViewScript.match(/const LOCATION_REQUIRED_EXPENSE_TYPES = new Set\(\[[\s\S]*?\]\)/)?.[0] || ''

  assert.match(locationRequiredBlock, /'travel'/)
  assert.match(locationRequiredBlock, /'meeting'/)
  assert.match(locationRequiredBlock, /'entertainment'/)
  assert.doesNotMatch(locationRequiredBlock, /'train_ticket'/)
  assert.doesNotMatch(locationRequiredBlock, /'flight_ticket'/)
  assert.doesNotMatch(locationRequiredBlock, /'ride_ticket'/)
  assert.match(
    detailViewScript,
    /const locationRequired = isLocationRequiredExpenseType\(item\.itemType\)[\s\S]*if \(locationRequired && isPlaceholderValue\(item\.itemLocation\)\) \{[\s\S]*issues\.push\('缺少地点'\)/
  )
  assert.doesNotMatch(detailViewScript, /完善第 1 条费用明细的业务地点/)
})

test('return reason dialog is wired into approval and detail return actions', () => {
  assert.match(returnReasonDialog, /missing_attachment/)
  assert.match(returnReasonDialog, /invoice_mismatch/)
  assert.match(returnReasonDialog, /APPLICATION_RETURN_REASON_OPTIONS/)
  assert.match(returnReasonDialog, /application_info_incomplete/)
  assert.match(returnReasonDialog, /application_business_need_unclear/)
  assert.match(returnReasonDialog, /application_budget_basis_missing/)
  assert.match(returnReasonDialog, /application_policy_mismatch/)
  assert.match(returnReasonDialog, /application_attachment_needed/)
  assert.match(returnReasonDialog, /application_other/)
  assert.match(returnReasonDialog, /退单选项/)
  assert.match(returnReasonDialog, /selectionError/)
  assert.match(returnReasonDialog, /selectedApplicationCode/)
  assert.match(returnReasonDialog, /application \? 'radio' : 'checkbox'/)
  assert.match(returnReasonDialog, /selectedReasonCodes\.value\.length === 0/)
  assert.match(returnReasonDialog, /lastAutoReason/)
  assert.match(returnReasonDialog, /reason_codes/)
  assert.match(approvalCenterTemplate, /<TravelRequestDetailView/)
  assert.doesNotMatch(approvalCenterTemplate, /<ReturnReasonDialog/)
  assert.match(detailViewTemplate, /<TravelRequestReturnDialog/)
  assert.match(detailViewTemplate, /:application="isApplicationDocument"/)
  assert.doesNotMatch(approvalCenterScript, /returnExpenseClaim/)
  assert.match(detailViewScript, /returnExpenseClaim\(request\.value\.claimId, payload\)/)
  assert.doesNotMatch(approvalCenterScript, /审批中心退回/)
  assert.doesNotMatch(detailViewScript, /详情页退回/)
})
