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

import {
  AI_APPLICATION_ACTION_SAVE_DRAFT,
  AI_APPLICATION_ACTION_SUBMIT,
  useTravelReimbursementApplicationPreviewActions
} from '../src/views/scripts/useTravelReimbursementApplicationPreviewActions.js'
import {
  normalizeSnapshotMessage,
  serializeSessionMessages
} from '../src/views/scripts/travelReimbursementConversationStateModel.js'
import { useTravelReimbursementApplicationSubmitConfirm } from '../src/views/scripts/useTravelReimbursementApplicationSubmitConfirm.js'

const messageItemTemplate = readFileSync(
  fileURLToPath(new URL('../src/components/travel/TravelReimbursementMessageItem.vue', import.meta.url)),
  'utf8'
)
const memoryPanelTemplate = readFileSync(
  fileURLToPath(new URL('../src/components/travel/TravelReimbursementMemoryPanel.vue', import.meta.url)),
  'utf8'
)
const createViewUiScript = readFileSync(
  fileURLToPath(new URL('../src/views/scripts/useTravelReimbursementCreateViewUi.js', import.meta.url)),
  'utf8'
)

function createPreview(overrides = {}) {
  return {
    fields: {
      applicationType: '差旅费用申请',
      time: '2026-07-20 至 2026-07-22',
      location: '上海',
      reason: '客户现场实施',
      days: '3天',
      transportMode: '火车',
      amount: '1800元'
    },
    ...overrides
  }
}

function createActions(overrides = {}) {
  const persisted = []
  const emitted = []
  const toasts = []
  const state = {
    conversationId: { value: 'conversation-financial-assistant' },
    currentUser: { value: { username: 'zhangsan', name: '张三' } },
    draftClaimId: { value: '' },
    linkedRequest: { value: null },
    reviewActionBusy: { value: false },
    sessionSwitchBusy: { value: false },
    submitting: { value: false },
    emitDraftSaved: (payload) => emitted.push(payload),
    entrySource: 'application',
    persistSessionState: () => persisted.push(Date.now()),
    toast: (message) => toasts.push(message),
    ...overrides
  }
  return {
    actions: useTravelReimbursementApplicationPreviewActions(state),
    emitted,
    persisted,
    state,
    toasts
  }
}

test('完整 preview 展示前写入稳定签发/动作 request id，并使用 canonical decision', async () => {
  const targetMessage = { id: 'preview-1', applicationPreview: null }
  const { actions, persisted } = createActions()
  let pendingIssueRequestId = ''
  let pendingActionRequestId = ''

  global.fetch = async (_url, options) => {
    const body = JSON.parse(options.body)
    pendingIssueRequestId = targetMessage.pendingApplicationPreviewDecision?.decisionIssueRequestId
    pendingActionRequestId = targetMessage.pendingApplicationPreviewDecision?.decisionActionRequestId
    assert.equal(body.request_id, pendingIssueRequestId)
    assert.ok(pendingActionRequestId.startsWith('action:'))
    assert.equal(targetMessage.applicationPreview, null)
    return {
      ok: true,
      async json() {
        return {
          decision_id: 'decision-assistant-1',
          decision_source: 'rule',
          expires_at: '2026-07-14T10:30:00Z',
          application_preview: {
            fields: {
              reason: '服务端规范事由',
              location: '上海'
            }
          }
        }
      }
    }
  }

  const preview = await actions.registerApplicationPreviewDecision({
    applicationPreview: createPreview(),
    sourceText: '7月20日去上海做客户现场实施，坐火车',
    targetMessage
  })

  assert.equal(preview.decisionId, 'decision-assistant-1')
  assert.equal(preview.decisionTrackingStatus, 'registered')
  assert.equal(preview.fields.reason, '服务端规范事由')
  assert.equal(preview.decisionIssueRequestId, pendingIssueRequestId)
  assert.equal(preview.decisionActionRequestId, pendingActionRequestId)
  assert.equal(targetMessage.pendingApplicationPreviewDecision, undefined)
  assert.ok(persisted.length >= 2)
})

test('仅缺出行方式时仍请求服务端，让 active memory 补齐并签发', async () => {
  const targetMessage = { id: 'preview-memory-resolution', applicationPreview: null }
  const { actions } = createActions()
  let requestCount = 0
  global.fetch = async (_url, options) => {
    requestCount += 1
    const body = JSON.parse(options.body)
    assert.match(body.message, /客户现场实施/)
    return {
      ok: true,
      async json() {
        return {
          decision_id: 'decision-memory-resolution',
          decision_source: 'hybrid',
          expires_at: '2026-07-14T10:30:00Z',
          application_preview: {
            fields: {
              ...createPreview().fields,
              transportMode: '火车',
              amount: '2880元'
            },
            memoryApplications: [{
              memory_id: 'memory-transport-active',
              field_key: 'transport_mode',
              value: '火车',
              status: 'applied',
              evidence_count: 3,
              approved_evidence_count: 2
            }]
          }
        }
      }
    }
  }

  const preview = await actions.registerApplicationPreviewDecision({
    applicationPreview: createPreview({
      fields: {
        ...createPreview().fields,
        transportMode: ''
      }
    }),
    sourceText: '7月20日去上海做客户现场实施',
    targetMessage
  })

  assert.equal(requestCount, 1)
  assert.equal(preview.readyToSubmit, true)
  assert.equal(preview.fields.transportMode, '火车')
  assert.equal(preview.decisionId, 'decision-memory-resolution')
  assert.equal(preview.memoryApplications[0].memoryId, 'memory-transport-active')
})

test('签发失败后 fail-closed，可编辑并以相同 request id 重试签发', async () => {
  const targetMessage = { id: 'preview-2', applicationPreview: null }
  const { actions } = createActions()
  const requestIds = []
  let attempt = 0
  global.fetch = async (_url, options) => {
    attempt += 1
    requestIds.push(JSON.parse(options.body).request_id)
    if (attempt === 1) {
      return {
        ok: false,
        status: 503,
        async json() {
          return { detail: '签发服务暂不可用' }
        }
      }
    }
    return {
      ok: true,
      async json() {
        return {
          decision_id: 'decision-assistant-retry',
          application_preview: { fields: { reason: '服务端规范事由' } }
        }
      }
    }
  }

  const failed = await actions.registerApplicationPreviewDecision({
    applicationPreview: createPreview(),
    sourceText: '去上海做客户现场实施',
    targetMessage
  })
  assert.equal(failed.decisionTrackingStatus, 'registration_failed')
  assert.equal(failed.decisionId, '')
  assert.equal(actions.canSaveApplicationPreviewDraft(targetMessage), false)
  await assert.rejects(
    actions.runApplicationPreviewAction(AI_APPLICATION_ACTION_SUBMIT, targetMessage),
    /尚未通过服务端签发/
  )

  targetMessage.applicationPreview.fields.reason = '用户修正后的事由'
  const retried = await actions.retryApplicationPreviewDecision(targetMessage)
  assert.equal(retried, true)
  assert.equal(requestIds[1], requestIds[0])
  assert.equal(targetMessage.applicationPreview.fields.reason, '用户修正后的事由')
  assert.equal(targetMessage.applicationPreview.decisionId, 'decision-assistant-retry')
})

test('动作失败重试复用 action request id；保存成功更新草稿和 next decision', async () => {
  const message = {
    id: 'preview-3',
    applicationPreview: createPreview({
      decisionId: 'decision-assistant-action',
      decisionTrackingStatus: 'registered',
      decisionActionRequestId: 'action:stable-retry'
    }),
    draftPayload: null
  }
  const { actions, emitted, state } = createActions()
  const actionBodies = []
  let attempt = 0
  global.fetch = async (_url, options) => {
    attempt += 1
    actionBodies.push(JSON.parse(options.body))
    if (attempt === 1) {
      return {
        ok: false,
        status: 503,
        async json() {
          return { detail: '动作服务暂不可用' }
        }
      }
    }
    return {
      ok: true,
      async json() {
        return {
          status: 'succeeded',
          result: {
            draft_payload: {
              claim_id: 'claim-assistant-draft',
              claim_no: 'AP-20260714-001',
              approval_stage: '待提交'
            },
            decision_id: 'decision-assistant-next',
            decision_expires_at: '2026-07-14T11:00:00Z',
            learning_receipts: [{
              memoryId: 'memory-reason-candidate',
              fieldKey: 'reason',
              fieldLabel: '申请事由',
              value: '客户现场实施',
              status: 'candidate',
              evidenceCount: 2,
              approvedEvidenceCount: 1,
              message: '已记录本次修正，继续确认后可形成稳定偏好'
            }]
          }
        }
      }
    }
  }

  await assert.rejects(
    actions.runApplicationPreviewAction(AI_APPLICATION_ACTION_SAVE_DRAFT, message),
    /动作服务暂不可用/
  )
  assert.equal(message.applicationLearningReceipts, undefined)
  assert.equal(message.applicationPreview.decisionActionRequestId, 'action:stable-retry')
  await actions.saveApplicationPreviewDraft(message)

  assert.equal(actionBodies[0].request_id, 'action:stable-retry')
  assert.equal(actionBodies[1].request_id, 'action:stable-retry')
  assert.equal(actionBodies[1].context_json.entry_source, 'application')
  assert.equal(actionBodies[1].context_json.source, 'financial_assistant')
  assert.equal(message.draftPayload.claim_id, 'claim-assistant-draft')
  assert.equal(state.draftClaimId.value, 'claim-assistant-draft')
  assert.equal(message.applicationPreview.decisionId, 'decision-assistant-next')
  assert.notEqual(message.applicationPreview.decisionActionRequestId, 'action:stable-retry')
  assert.equal(message.applicationLearningReceipts.length, 1)
  assert.equal(message.applicationLearningReceipts[0].status, 'candidate')
  assert.equal(emitted[0].status, 'draft')
})

test('仅展示已实际填入当前字段的 applied memory，忘记后保留当前字段', async () => {
  const originalFields = createPreview().fields
  const message = {
    id: 'preview-memory',
    applicationPreview: createPreview({
      memoryApplications: [
        {
          memoryId: 'memory-transport-applied',
          fieldKey: 'transport_mode',
          fieldLabel: '出行方式',
          value: '火车',
          status: 'applied',
          evidenceCount: 4,
          approvedEvidenceCount: 3,
          message: '根据已审批的历史差旅申请填入'
        },
        {
          memoryId: 'memory-reason-candidate',
          fieldKey: 'reason',
          fieldLabel: '申请事由',
          value: '客户现场实施',
          status: 'candidate',
          evidenceCount: 2,
          approvedEvidenceCount: 1,
          message: '候选偏好'
        },
        {
          memoryId: 'memory-location-stale',
          fieldKey: 'location',
          fieldLabel: '出差地点',
          value: '北京',
          status: 'applied',
          evidenceCount: 3,
          approvedEvidenceCount: 2,
          message: '旧偏好值与当前字段不一致'
        }
      ]
    })
  }
  let confirmCalls = 0
  let capturedUrl = ''
  const { actions, persisted, toasts } = createActions({
    confirmForgetMemory: async () => {
      confirmCalls += 1
      return true
    }
  })
  assert.deepEqual(
    actions.resolveAppliedApplicationPreviewMemories(message).map((item) => item.memoryId),
    ['memory-transport-applied']
  )

  global.fetch = async (url, options) => {
    capturedUrl = String(url)
    assert.equal(options.method, 'DELETE')
    return {
      ok: true,
      async json() {
        return { status: 'forgotten' }
      }
    }
  }
  const forgotten = await actions.forgetApplicationPreviewMemory(
    message,
    message.applicationPreview.memoryApplications[0]
  )
  assert.equal(forgotten, true)
  assert.equal(confirmCalls, 1)
  assert.equal(
    capturedUrl,
    '/api/v1/expense-application-memories/memory-transport-applied'
  )
  assert.deepEqual(message.applicationPreview.fields, originalFields)
  assert.deepEqual(
    message.applicationPreview.memoryApplications.map((item) => item.memoryId),
    ['memory-reason-candidate', 'memory-location-stale']
  )
  assert.ok(persisted.length >= 1)
  assert.match(toasts.at(-1), /当前申请内容保持不变/)
})

test('忘记偏好失败时不移除 memory，也不改当前字段', async () => {
  const message = {
    id: 'preview-memory-failure',
    applicationPreview: createPreview({
      memoryApplications: [{
        memoryId: 'memory-failure',
        fieldKey: 'reason',
        fieldLabel: '申请事由',
        value: '客户现场实施',
        status: 'applied'
      }]
    })
  }
  const previousPreview = structuredClone(message.applicationPreview)
  const { actions, toasts } = createActions({
    confirmForgetMemory: () => true
  })
  global.fetch = async () => ({
    ok: false,
    status: 503,
    async json() {
      return { detail: '偏好服务暂不可用' }
    }
  })

  const forgotten = await actions.forgetApplicationPreviewMemory(
    message,
    message.applicationPreview.memoryApplications[0]
  )
  assert.equal(forgotten, false)
  assert.deepEqual(message.applicationPreview, previousPreview)
  assert.match(toasts.at(-1), /偏好服务暂不可用/)
})

test('组织记忆不允许普通用户调用忘记接口', async () => {
  const organizationMemory = {
    memoryId: 'memory-enterprise-policy',
    fieldKey: 'transport_mode',
    fieldLabel: '出行方式',
    value: '火车',
    status: 'applied',
    scopeType: 'enterprise',
    canRevoke: false
  }
  const message = {
    id: 'preview-enterprise-memory',
    applicationPreview: createPreview({ memoryApplications: [organizationMemory] })
  }
  const { actions, toasts } = createActions()
  let requestCount = 0
  global.fetch = async () => {
    requestCount += 1
    throw new Error('不应发起组织记忆删除请求')
  }

  const forgotten = await actions.forgetApplicationPreviewMemory(message, organizationMemory)

  assert.equal(forgotten, false)
  assert.equal(requestCount, 0)
  assert.equal(message.applicationPreview.memoryApplications.length, 1)
  assert.match(toasts.at(-1), /管理员统一维护/)
})

test('申请预览 UI 解释分层记忆，仅个人记忆提供忘记入口', () => {
  assert.match(messageItemTemplate, /<TravelReimbursementMemoryPanel :message="message" :ui="ui" \/>/)
  assert.match(memoryPanelTemplate, /resolveAppliedApplicationPreviewMemories\(props\.message\)/)
  assert.match(memoryPanelTemplate, /企业制度填充/)
  assert.match(memoryPanelTemplate, /部门基线填充/)
  assert.match(memoryPanelTemplate, /个性化填充/)
  assert.match(memoryPanelTemplate, /您仍可直接修改上方字段/)
  assert.match(memoryPanelTemplate, /忘记此偏好/)
  assert.match(memoryPanelTemplate, /v-if="canForgetMemory\(memory\)"/)
  assert.match(memoryPanelTemplate, /memory\.scopeType === 'user' && memory\.canRevoke === true/)
  assert.match(memoryPanelTemplate, /:aria-busy="ui\.isForgettingApplicationMemory\(memory\)"/)
  assert.match(memoryPanelTemplate, /检测到同层级设置冲突/)
  assert.doesNotMatch(memoryPanelTemplate, /JSON\.stringify\(memory\.conflicts/)
  assert.match(memoryPanelTemplate, /resolveVisibleApplicationLearningReceipts\(props\.message\)/)
  assert.match(memoryPanelTemplate, /ui\.resolveApplicationLearningReceiptTitle\(receipt\)/)
  assert.doesNotMatch(messageItemTemplate, /已按历史偏好填入/)
  assert.match(createViewUiScript, /forgetApplicationPreviewMemory: ctx\.forgetApplicationPreviewMemory/)
})

test('学习回执仅展示 candidate/applied，并兼容旧 active 状态', () => {
  const { actions } = createActions()
  const receipts = actions.resolveVisibleApplicationLearningReceipts({
    applicationLearningReceipts: [
      {
        memoryId: 'memory-candidate',
        fieldKey: 'reason',
        fieldLabel: '申请事由',
        value: '客户现场实施',
        status: 'candidate'
      },
      {
        memoryId: 'memory-active-legacy',
        fieldKey: 'transport_mode',
        fieldLabel: '出行方式',
        value: '火车',
        status: 'active'
      },
      {
        memoryId: 'memory-revoked',
        fieldKey: 'transport_mode',
        fieldLabel: '出行方式',
        value: '飞机',
        status: 'revoked'
      }
    ]
  })

  assert.deepEqual(receipts.map((item) => item.status), ['candidate', 'applied'])
  assert.equal(actions.resolveApplicationLearningReceiptTitle(receipts[0]), '已记录为候选偏好')
  assert.equal(actions.resolveApplicationLearningReceiptTitle(receipts[1]), '偏好已激活')
})

test('decision 过期后清空旧 ID 并开放重新签发', async () => {
  const message = {
    id: 'preview-expired',
    applicationPreview: createPreview({
      decisionId: 'decision-expired',
      decisionTrackingStatus: 'registered',
      decisionIssueRequestId: 'preview:old',
      decisionActionRequestId: 'action:expired'
    })
  }
  const { actions } = createActions()
  global.fetch = async () => ({
    ok: false,
    status: 409,
    async json() {
      return { detail: '该预览决策已过期，请重新生成预览。' }
    }
  })

  await assert.rejects(
    actions.runApplicationPreviewAction(AI_APPLICATION_ACTION_SUBMIT, message),
    /已过期/
  )
  assert.equal(message.applicationPreview.decisionId, '')
  assert.equal(message.applicationPreview.decisionTrackingStatus, 'registration_failed')
  assert.notEqual(message.applicationPreview.decisionIssueRequestId, 'preview:old')
  assert.equal(message.applicationPreview.decisionActionRequestId, '')
  assert.equal(actions.canRetryApplicationPreviewDecision(message), true)
})

test('本地快照保留签发与动作 request id，并把中断中的签发恢复为可重试状态', () => {
  const pending = createPreview({
    decisionIssueRequestId: 'preview:recover',
    decisionActionRequestId: 'action:recover',
    decisionTrackingStatus: 'registering'
  })
  const serialized = serializeSessionMessages([{
    id: 'preview-4',
    role: 'assistant',
    text: '正在签发',
    attachments: [],
    pendingApplicationPreviewDecision: pending
  }])
  assert.equal(
    serialized[0].pendingApplicationPreviewDecision.decisionIssueRequestId,
    'preview:recover'
  )

  const restored = normalizeSnapshotMessage(serialized[0])
  assert.equal(restored.applicationPreview.decisionIssueRequestId, 'preview:recover')
  assert.equal(restored.applicationPreview.decisionActionRequestId, 'action:recover')
  assert.equal(restored.applicationPreview.decisionTrackingStatus, 'registration_failed')
})

function createSubmitConfirm({ message, runApplicationPreviewAction, submitComposer }) {
  const applicationSubmitConfirmDialog = { value: { open: true, message } }
  const reviewActionBusy = { value: false }
  const toasts = []
  const api = useTravelReimbursementApplicationSubmitConfirm({
    activeSessionType: { value: 'application' },
    applicationSubmitConfirmDialog,
    buildStewardFieldItems: () => [],
    createMessage: (role, text, _attachments, extras = {}) => ({ role, text, ...extras }),
    emitDraftSaved: () => {},
    formatStewardMissingFieldList: () => '',
    formatStewardOntologyFields: () => '',
    linkedRequest: { value: null },
    messages: { value: [] },
    nextTick: (callback) => callback?.(),
    persistSessionState: () => {},
    reviewActionBusy,
    runApplicationPreviewAction,
    scrollToBottom: () => {},
    submitComposer,
    submitting: { value: false },
    toast: (text) => toasts.push(text)
  })
  return { api, toasts }
}

test('structured preview 只走认证 action；未签发失败时也不降级到 Orchestrator', async () => {
  let actionCalls = 0
  let orchestratorCalls = 0
  const unsignedMessage = {
    applicationPreview: createPreview({
      decisionTrackingStatus: 'registration_failed',
      decisionId: ''
    })
  }
  const { api, toasts } = createSubmitConfirm({
    message: unsignedMessage,
    runApplicationPreviewAction: async () => {
      actionCalls += 1
      throw new Error('申请预览尚未通过服务端签发')
    },
    submitComposer: async () => {
      orchestratorCalls += 1
      return null
    }
  })

  await api.confirmApplicationSubmit()
  assert.equal(actionCalls, 1)
  assert.equal(orchestratorCalls, 0)
  assert.match(toasts[0], /尚未通过服务端签发/)
})

test('旧消息没有 structured preview 时保留 Orchestrator fallback', async () => {
  let actionCalls = 0
  let orchestratorCalls = 0
  const { api } = createSubmitConfirm({
    message: { text: '旧版确认消息' },
    runApplicationPreviewAction: async () => {
      actionCalls += 1
      return null
    },
    submitComposer: async () => {
      orchestratorCalls += 1
      return { status: 'succeeded', result: {} }
    }
  })

  await api.confirmApplicationSubmit()
  assert.equal(actionCalls, 0)
  assert.equal(orchestratorCalls, 1)
})
