import { flushPromises, mount } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createI18n } from 'vue-i18n'
import { createMemoryHistory, createRouter } from 'vue-router'

import zhCN from '@/i18n/locales/zh-CN'
import ReservationOrderDetailView from '@/views/reservation/ReservationOrderDetailView.vue'
import ReservationOrderListView from '@/views/reservation/ReservationOrderListView.vue'
import ReservationSourceMessageConversationView from '@/views/reservation/ReservationSourceMessageConversationView.vue'
import ReservationTaskListView from '@/views/reservation/ReservationTaskListView.vue'
import { useAuthStore } from '@/stores/authStore'
import type { ReservationTaskListResult, SourceMessageConversationResult } from '@/types/reservation'

vi.mock('@/services/reservationService', async (importOriginal) => {
  const actual = await importOriginal<typeof import('@/services/reservationService')>()
  return {
    ...actual,
    fetchReservationOrderDetail: vi.fn(),
    fetchReservationOrders: vi.fn(),
    fetchReservationTaskList: vi.fn(),
    fetchSourceMessageConversation: vi.fn(),
  }
})

const service = await import('@/services/reservationService')

function createOrderListResult(
  displayOrderKey = 'GRP-001',
  page: { page_num: number; page_size: number; total: number } = {
    page_num: 1,
    page_size: 20,
    total: 1,
  },
) {
  return {
    items: [
      {
        order_id: displayOrderKey === 'GRP-002' ? '20002' : '20001',
        hotel_id: 'HOTEL-TEST',
        order_status: 'ACTIVE',
        display_order_key: displayOrderKey,
        temporary_order_no: null,
        confirmation_number: null,
        group_code: displayOrderKey,
        display_name: displayOrderKey,
        open_task_count: 2,
        next_processable_task_id: '10002',
        created_at: '2026-07-08T03:00:00Z',
        updated_at: '2026-07-08T03:10:00Z',
      },
    ],
    page,
  }
}

function createTaskListResult(
  displayOrderKey: string,
  sourceSubject: string,
  page: { page_num: number; page_size: number; total: number } = {
    page_num: 1,
    page_size: 20,
    total: 1,
  },
): ReservationTaskListResult {
  return {
    items: [
      {
        task_id: displayOrderKey === 'GRP-NEW' ? '10002' : '10001',
        order_id: '20001',
        hotel_id: 'HOTEL-TEST',
        display_order_key: displayOrderKey,
        temporary_order_no: null,
        task_type: 'NEW_BOOKING',
        task_subtype: 'NEW_BOOKING',
        task_status: 'PENDING_CONFIRM',
        card_name: 'New Booking',
        queue_sequence: 1,
        queue_participation: true,
        can_process: true,
        readonly_reason_code: null,
        source_message_id: '30001',
        source_subject: sourceSubject,
        source_sender_summary: null,
        source_received_at: null,
        external_conversation_id: null,
        conversation_message_count: null,
        created_at: '2026-07-08T03:00:00Z',
        updated_at: '2026-07-08T03:10:00Z',
      },
    ],
    page,
  }
}

function createSourceMessageOnlyTaskListResult(taskSubtype: 'S10' | 'S99' = 'S10') {
  return {
    items: [
      {
        task_id: taskSubtype === 'S10' ? '19990' : '19999',
        order_id: taskSubtype === 'S10' ? '29990' : '29999',
        hotel_id: 'HOTEL-TEST',
        display_order_key: null,
        temporary_order_no: null,
        task_type: 'MESSAGE_NOTIFICATION',
        task_subtype: taskSubtype,
        result_type: 'source_message_review_notification',
        ai_task_type: 'Message Notification',
        route_code: taskSubtype,
        system_process_category: 'SOURCE_MESSAGE_NOTIFICATION',
        task_status: 'COMPLETED',
        card_name: 'Message Notification',
        queue_sequence: null,
        queue_participation: false,
        can_process: false,
        readonly_reason_code: 'QUEUE_EXCLUDED',
        source_message_id: taskSubtype === 'S10' ? '39990' : '39999',
        source_subject: taskSubtype === 'S10' ? 'FYI only' : 'Unclear email',
        source_sender_summary: 'guest@example.test',
        source_received_at: '2026-07-08T03:00:00Z',
        external_conversation_id: 'thread-source-only',
        conversation_message_count: 1,
        created_at: '2026-07-08T03:00:00Z',
        updated_at: '2026-07-08T03:10:00Z',
      },
    ],
    page: {
      page_num: 1,
      page_size: 20,
      total: 1,
    },
  }
}

function createParentGroupCancelAllotmentTaskListResult(): ReservationTaskListResult {
  const result = createTaskListResult('PARENT-2608-BLOCK', 'Parent split confirmation')
  result.items[0] = {
    ...result.items[0]!,
    task_type: 'CANCEL_BOOKING',
    task_subtype: 'cancel_allotment_control_block',
    result_type: 'normal_task',
    ai_task_type: 'Cancel Allotment',
    route_code: 'R08_CANCEL_ALLOTMENT_CONTROL_BLOCK_NORMAL',
    system_process_category: 'BUSINESS_TASK',
    card_name: 'CANCEL_ALLOTMENT',
  }
  return result
}

function createOrderDetailResult(displayName = 'GRP-001') {
  return {
    order: {
      order_id: displayName === 'GRP-002' ? '20002' : '20001',
      hotel_id: 'HOTEL-TEST',
      order_status: 'ACTIVE',
      temporary_order_no: null,
      confirmation_number: null,
      group_code: displayName,
      block_code: null,
      allotment_code: null,
      display_name: displayName,
      created_at: '2026-07-08T03:00:00Z',
      updated_at: '2026-07-08T03:10:00Z',
    },
    tasks: [
      {
        task_id: '10001',
        task_type: 'NEW_BOOKING',
        task_subtype: 'NEW_BOOKING',
        task_status: 'PENDING_CONFIRM',
        card_name: 'New Booking',
        queue_sequence: 1,
        queue_participation: true,
        can_process: true,
        readonly_reason_code: null,
        source_message_id: '30001',
        source_subject: 'Booking Request',
        source_sender_summary: 'guest@example.test',
        source_received_at: '2026-07-08T03:00:00Z',
        external_conversation_id: null,
        conversation_message_count: 2,
        created_at: '2026-07-08T03:00:00Z',
      },
      {
        task_id: '10002',
        task_type: 'UPDATE_BOOKING',
        task_subtype: 'RATE_CHANGE',
        task_status: 'BLOCKED',
        card_name: 'Rate Change',
        queue_sequence: 2,
        queue_participation: true,
        can_process: false,
        readonly_reason_code: 'PREVIOUS_TASK_NOT_FINISHED',
        source_message_id: '30002',
        source_subject: 'Booking Update',
        source_sender_summary: 'agent@example.test',
        source_received_at: '2026-07-08T04:00:00Z',
        external_conversation_id: null,
        conversation_message_count: 3,
        created_at: '2026-07-08T04:00:00Z',
      },
    ],
    warnings: [],
  }
}

function createSourceMessageConversationResult(
  overrides: Partial<SourceMessageConversationResult> = {},
): SourceMessageConversationResult {
  const result = createSourceMessageConversationResultBase()
  return {
    ...result,
    ...overrides,
  }
}

function createSourceMessageConversationResultBase(): SourceMessageConversationResult {
  return {
    conversation: {
      hotel_id: 'HOTEL-TEST',
      channel: 'EMAIL',
      external_conversation_id: 'thread-30001',
      subject: 'Booking Request',
      message_count: 2,
      first_received_at: '2026-07-08T02:00:00Z',
      last_received_at: '2026-07-08T03:00:00Z',
    },
    messages: [
      {
        id: '30001',
        external_message_id: 'msg-30001',
        external_conversation_id: 'thread-30001',
        sender_summary: 'guest@example.test',
        subject: 'Booking Request',
        received_at: '2026-07-08T02:00:00Z',
        source_sent_at: '2026-07-08T01:58:00Z',
        text_body: '完整邮件正文',
        html_body: '<script>alert("raw")</script><strong onclick="alert(1)">原始邮件正文</strong>',
        html_body_sanitized: '<strong>清洗后邮件正文</strong>',
        html_sanitize_required: true,
        html_render_mode: 'SANITIZED_HTML',
        inline_images: [],
        attachments: [
          {
            mediaType: 'ATTACHMENT',
            fileName: 'rooming-list.xlsx',
            contentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
            sizeBytes: 1024,
            externalUrl: 'https://example.test/rooming-list.xlsx',
            externalMediaId: 'media-1',
          },
        ],
        related_orders: [
          {
            order_id: '20001',
            display_order_key: 'GRP-001',
            order_status: 'ACTIVE',
          },
        ],
        related_tasks: [
          {
            task_id: '10001',
            order_id: '20001',
            task_type: 'NEW_BOOKING',
            task_subtype: 'NEW_BOOKING',
            task_status: 'PENDING_CONFIRM',
            card_name: 'New Booking',
          },
        ],
      },
    ],
  }
}

function createAttachment(overrides: Record<string, unknown> = {}) {
  return {
    mediaType: 'ATTACHMENT',
    fileName: 'rooming-list.xlsx',
    contentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
    sizeBytes: 1024,
    externalUrl: 'https://example.test/rooming-list.xlsx',
    externalMediaId: 'media-1',
    ...overrides,
  }
}

function createDeferred<T>() {
  let resolve!: (value: T) => void
  let reject!: (reason?: unknown) => void
  const promise = new Promise<T>((promiseResolve, promiseReject) => {
    resolve = promiseResolve
    reject = promiseReject
  })
  return { promise, resolve, reject }
}

function createAuthPayload() {
  return {
    access_token: 'session-token',
    token_type: 'Bearer',
    expires_at: '2026-07-09T12:00:00Z',
    user: {
      id: '1900000000000000001',
      username: 'admin',
      display_name: '系统管理员',
      super_admin: true,
    },
    default_hotel_id: 'HOTEL-TEST',
    hotels: [
      {
        hotel_id: 'HOTEL-TEST',
        hotel_name: '测试酒店',
        time_zone: 'Asia/Bangkok',
        default_hotel: true,
      },
      {
        hotel_id: 'HOTEL-BKK',
        hotel_name: '曼谷酒店',
        time_zone: 'Asia/Bangkok',
        default_hotel: false,
      },
    ],
    permissions: ['HOTEL_SWITCH'],
    menus: [],
  }
}

async function mountWithPlugins(component: object, initialPath = '/', stubs: Record<string, unknown> = {}) {
  const pinia = createPinia()
  setActivePinia(pinia)
  const authStore = useAuthStore()
  authStore.applyLoginResult(createAuthPayload())
  const i18n = createI18n({
    legacy: false,
    locale: 'zh-CN',
    messages: {
      'zh-CN': zhCN,
    },
  })
  const router = createRouter({
    history: createMemoryHistory(),
    routes: [
      { path: '/', component: { template: '<div />' } },
      { path: '/reservation/orders/:orderId', component: { template: '<div />' } },
      { path: '/reservation/tasks/:taskId', component: { template: '<div />' } },
      { path: '/reservation/source-messages/:sourceMessageId/conversation', component: { template: '<div />' } },
    ],
  })
  await router.push(initialPath)
  await router.isReady()

  return mount(component, {
    global: {
      plugins: [pinia, i18n, router],
      stubs: {
        RouterLink: {
          template: '<a><slot /></a>',
        },
        ...stubs,
      },
    },
  })
}

describe('reservation P0 views', () => {
  beforeEach(() => {
    sessionStorage.clear()
    localStorage.clear()
    vi.mocked(service.fetchReservationOrderDetail).mockReset()
    vi.mocked(service.fetchReservationOrders).mockReset()
    vi.mocked(service.fetchReservationTaskList).mockReset()
    vi.mocked(service.fetchSourceMessageConversation).mockReset()
  })

  it('renders active order task source evidence without the stale pending copy', async () => {
    vi.mocked(service.fetchReservationOrderDetail).mockResolvedValue(createOrderDetailResult())

    const wrapper = await mountWithPlugins(ReservationOrderDetailView, '/reservation/orders/20001', {
      ReservationTaskDetailPanel: {
        template: '<section />',
      },
    })
    await vi.dynamicImportSettled()

    expect(wrapper.text()).toContain('Booking Request')
    expect(wrapper.text()).toContain('guest@example.test')
    expect(wrapper.text()).toContain('2')
    expect(wrapper.text()).toContain('前序任务未完成')
    expect(wrapper.text()).toContain('查看邮件会话')
    expect(wrapper.text()).not.toContain('PREVIOUS_TASK_NOT_FINISHED')
    expect(wrapper.text()).not.toContain('来源邮件摘要字段待后端补充')
  })

  it('renders order list items returned by the backend', async () => {
    vi.mocked(service.fetchReservationOrders).mockResolvedValue(createOrderListResult())

    const wrapper = await mountWithPlugins(ReservationOrderListView)
    await vi.dynamicImportSettled()

    expect(wrapper.text()).toContain('GRP-001')
    expect(wrapper.text()).toContain('有效')
    expect(wrapper.text()).toContain('继续处理')
    expect(wrapper.text()).not.toContain('接口待接入')
  })

  it('filters order list with order status and business-key fields', async () => {
    vi.mocked(service.fetchReservationOrders).mockResolvedValue(createOrderListResult())

    const wrapper = await mountWithPlugins(ReservationOrderListView)
    await vi.dynamicImportSettled()

    await wrapper.find('select[name="order_status"]').setValue('LOGIC_DELETED')
    await vi.dynamicImportSettled()
    await wrapper.find('input[name="group_code"]').setValue('GRP-009')
    await vi.dynamicImportSettled()
    await wrapper.find('input[name="confirmation_number"]').setValue('CNF-009')
    await vi.dynamicImportSettled()

    expect(service.fetchReservationOrders).toHaveBeenLastCalledWith(
      expect.objectContaining({
        order_status: 'LOGIC_DELETED',
        group_code: 'GRP-009',
        confirmation_number: 'CNF-009',
        page_num: 1,
      }),
    )
  })

  it('paginates order list requests with backend page metadata', async () => {
    vi.mocked(service.fetchReservationOrders)
      .mockResolvedValueOnce(createOrderListResult('GRP-001', { page_num: 1, page_size: 20, total: 41 }))
      .mockResolvedValueOnce(createOrderListResult('GRP-002', { page_num: 2, page_size: 20, total: 41 }))

    const wrapper = await mountWithPlugins(ReservationOrderListView)
    await vi.dynamicImportSettled()

    expect(wrapper.text()).toContain('第 1 / 3 页')
    await wrapper.findAll('button').find((button) => button.text().includes('下一页'))?.trigger('click')
    await vi.dynamicImportSettled()

    expect(service.fetchReservationOrders).toHaveBeenLastCalledWith(
      expect.objectContaining({
        page_num: 2,
        page_size: 20,
      }),
    )
    expect(wrapper.text()).toContain('GRP-002')
    expect(wrapper.text()).toContain('第 2 / 3 页')
  })

  it('renders backend-normalized order pagination metadata', async () => {
    vi.mocked(service.fetchReservationOrders).mockResolvedValue(
      createOrderListResult('GRP-001', { page_num: 3, page_size: 10, total: 31 }),
    )

    const wrapper = await mountWithPlugins(ReservationOrderListView)
    await vi.dynamicImportSettled()

    expect(wrapper.text()).toContain('第 3 / 4 页')
  })

  it('reloads the order list when the selected hotel changes', async () => {
    vi.mocked(service.fetchReservationOrders)
      .mockResolvedValueOnce(createOrderListResult('GRP-001'))
      .mockResolvedValueOnce(createOrderListResult('GRP-002'))

    const wrapper = await mountWithPlugins(ReservationOrderListView)
    await vi.dynamicImportSettled()
    const authStore = useAuthStore()

    authStore.setSelectedHotelId('HOTEL-BKK')
    await vi.dynamicImportSettled()

    expect(service.fetchReservationOrders).toHaveBeenCalledTimes(2)
    expect(service.fetchReservationOrders).toHaveBeenLastCalledWith(
      expect.objectContaining({
        page_num: 1,
      }),
    )
    expect(wrapper.text()).toContain('GRP-002')
  })

  it('renders task list items returned by the backend', async () => {
    vi.mocked(service.fetchReservationTaskList).mockResolvedValue(createTaskListResult('GRP-001', 'Booking Request'))

    const wrapper = await mountWithPlugins(ReservationTaskListView)
    await vi.dynamicImportSettled()

    expect(wrapper.text()).toContain('GRP-001')
    expect(wrapper.text()).toContain('Booking Request')
    expect(wrapper.text()).toContain('新预订')
    expect(wrapper.text()).toContain('查看邮件会话')
    expect(wrapper.text()).not.toContain('接口待补')
    expect(wrapper.text()).not.toContain('NEW_BOOKING / NEW_BOOKING')
  })

  it('filters source-message-only tasks with S10 and S99 subtypes', async () => {
    vi.mocked(service.fetchReservationTaskList).mockResolvedValue(createSourceMessageOnlyTaskListResult('S10'))

    const wrapper = await mountWithPlugins(ReservationTaskListView)
    await vi.dynamicImportSettled()

    const taskTypeOptions = wrapper.find('select[name="task_type"]').findAll('option')
      .map((option) => option.attributes('value'))
    const taskSubtypeOptions = wrapper.find('select[name="task_subtype"]').findAll('option')
      .map((option) => option.attributes('value'))

    expect(taskTypeOptions).toEqual([
      '',
      'NEW_BOOKING',
      'UPDATE_BOOKING',
      'CANCEL_BOOKING',
      'MANUAL_REVIEW',
      'SOURCE_MESSAGE_ONLY',
    ])
    expect(taskTypeOptions).not.toContain('INFORMATIONAL_MESSAGE')
    expect(taskSubtypeOptions).toEqual([
      '',
      'NEW_BOOKING',
      'UPDATE_BOOKING',
      'CANCEL_BOOKING',
      'RATE_CHANGE',
      'UPDATE_STAY_DATES',
      'UPDATE_ROOM_TYPE',
      'UPDATE_FIX_CHARGE',
      'NEW_GROUP_BLOCK',
      'NEW_FIT_RESERVATION',
      'MANUAL_REVIEW',
      'S10',
      'S99',
    ])
    expect(taskSubtypeOptions).not.toContain('S000')
    expect(taskSubtypeOptions).not.toContain('S999')
    expect(taskSubtypeOptions).not.toContain('linked_parent_release_after_child_split')
    expect(wrapper.find('select[name="task_type"]').text()).toContain('来源消息只读')
    expect(wrapper.find('select[name="task_subtype"]').text()).toContain('纯信息类邮件（S10）')
    expect(wrapper.find('select[name="task_subtype"]').text()).toContain('无法形成业务素材包（S99）')

    await wrapper.find('select[name="task_type"]').setValue('SOURCE_MESSAGE_ONLY')
    await vi.dynamicImportSettled()
    await wrapper.find('select[name="task_subtype"]').setValue('S99')
    await vi.dynamicImportSettled()

    expect(service.fetchReservationTaskList).toHaveBeenLastCalledWith(
      expect.objectContaining({
        task_type: 'SOURCE_MESSAGE_ONLY',
        task_subtype: 'S99',
        page_num: 1,
      }),
    )
    const lastTaskRequest = vi.mocked(service.fetchReservationTaskList).mock.calls[
      vi.mocked(service.fetchReservationTaskList).mock.calls.length - 1
    ]?.[0] ?? {}
    expect(Object.keys(lastTaskRequest).sort()).toEqual([
      'keyword',
      'order_status',
      'page_num',
      'page_size',
      'queue_participation',
      'task_status',
      'task_subtype',
      'task_type',
    ])
    expect(lastTaskRequest).not.toHaveProperty('result_type')
    expect(lastTaskRequest).not.toHaveProperty('route_code')
    expect(lastTaskRequest).not.toHaveProperty('system_process_category')
  })

  it('renders R08 parent group cancel allotment as a business card', async () => {
    vi.mocked(service.fetchReservationTaskList).mockResolvedValue(createParentGroupCancelAllotmentTaskListResult())

    const wrapper = await mountWithPlugins(ReservationTaskListView)
    await vi.dynamicImportSettled()

    expect(wrapper.text()).toContain('PARENT-2608-BLOCK')
    expect(wrapper.text()).toContain('Parent Group')
    expect(wrapper.text()).toContain('取消 Parent Group 配额')
    expect(wrapper.text()).toContain('取消 Parent Group 整块配额')
    expect(wrapper.text()).toContain('查看订单')
    expect(wrapper.text()).not.toContain('适配契约异常')
    expect(wrapper.text()).not.toContain('linked_parent_release_after_child_split')
  })

  it('renders source-message-only task rows without a visible order action', async () => {
    vi.mocked(service.fetchReservationTaskList).mockResolvedValue(createSourceMessageOnlyTaskListResult('S10'))

    const wrapper = await mountWithPlugins(ReservationTaskListView)
    await vi.dynamicImportSettled()

    expect(wrapper.text()).toContain('来源消息只读')
    expect(wrapper.text()).toContain('纯信息类邮件（S10）')
    expect(wrapper.text()).toContain('来源消息通知')
    expect(wrapper.text()).toContain('FYI only')
    expect(wrapper.text()).toContain('查看任务')
    expect(wrapper.text()).toContain('查看邮件会话')
    expect(wrapper.text()).not.toContain('查看订单')
    expect(wrapper.text()).not.toContain('29990')
  })

  it('does not treat a business task as source-message-only only because subtype is S10', async () => {
    const result = createTaskListResult('GRP-S10-BUSINESS', 'Business booking request')
    result.items[0] = {
      ...result.items[0]!,
      task_type: 'NEW_BOOKING',
      task_subtype: 'S10',
      result_type: 'normal_task',
      ai_task_type: 'NEW_BOOKING',
      route_code: 'S10',
      system_process_category: 'BUSINESS_TASK',
      card_name: 'New Booking',
    }
    vi.mocked(service.fetchReservationTaskList).mockResolvedValue(result)

    const wrapper = await mountWithPlugins(ReservationTaskListView)
    await vi.dynamicImportSettled()

    const rowText = wrapper.find('tbody').text()
    expect(rowText).toContain('GRP-S10-BUSINESS')
    expect(rowText).toContain('20001')
    expect(rowText).toContain('查看订单')
    expect(rowText).not.toContain('来源消息只读')
    expect(rowText).not.toContain('不关联可见订单')
  })

  it('filters and paginates task list with supported backend fields', async () => {
    vi.mocked(service.fetchReservationTaskList)
      .mockResolvedValueOnce(createTaskListResult('GRP-001', 'Booking Request', { page_num: 1, page_size: 20, total: 45 }))
      .mockResolvedValueOnce(createTaskListResult('GRP-001', 'Booking Request', { page_num: 1, page_size: 20, total: 45 }))
      .mockResolvedValueOnce(createTaskListResult('GRP-001', 'Booking Request', { page_num: 1, page_size: 20, total: 45 }))
      .mockResolvedValueOnce(createTaskListResult('GRP-001', 'Booking Request', { page_num: 1, page_size: 20, total: 45 }))
      .mockResolvedValueOnce(createTaskListResult('GRP-001', 'Booking Request', { page_num: 1, page_size: 20, total: 45 }))
      .mockResolvedValueOnce(createTaskListResult('GRP-002', 'Booking Update', { page_num: 2, page_size: 20, total: 45 }))

    const wrapper = await mountWithPlugins(ReservationTaskListView)
    await vi.dynamicImportSettled()

    expect(wrapper.text()).not.toContain('订单状态筛选待后端接入')
    expect(wrapper.find('select[name="order_status"]').attributes('disabled')).toBeUndefined()
    expect(wrapper.find('select[name="task_status"]').text()).toContain('执行中')
    expect(wrapper.find('select[name="task_status"]').text()).toContain('失败')

    await wrapper.find('select[name="task_status"]').setValue('FAILED')
    await vi.dynamicImportSettled()
    await wrapper.find('select[name="task_subtype"]').setValue('RATE_CHANGE')
    await vi.dynamicImportSettled()
    await wrapper.find('select[name="order_status"]').setValue('ENDED')
    await vi.dynamicImportSettled()
    await wrapper.find('select[name="queue_participation"]').setValue('true')
    await vi.dynamicImportSettled()

    const taskListCalls = vi.mocked(service.fetchReservationTaskList).mock.calls
    const filteredTaskRequest = taskListCalls[taskListCalls.length - 1]?.[0] ?? {}
    expect(filteredTaskRequest).toMatchObject({
      task_status: 'FAILED',
      task_subtype: 'RATE_CHANGE',
      order_status: 'ENDED',
      queue_participation: true,
      page_num: 1,
    })

    await wrapper.findAll('button').find((button) => button.text().includes('下一页'))?.trigger('click')
    await vi.dynamicImportSettled()

    expect(service.fetchReservationTaskList).toHaveBeenLastCalledWith(
      expect.objectContaining({
        task_status: 'FAILED',
        task_subtype: 'RATE_CHANGE',
        order_status: 'ENDED',
        queue_participation: true,
        page_num: 2,
        page_size: 20,
      }),
    )
    expect(wrapper.text()).toContain('GRP-002')
    expect(wrapper.text()).toContain('第 2 / 3 页')
  })

  it('renders backend-normalized task pagination metadata', async () => {
    vi.mocked(service.fetchReservationTaskList).mockResolvedValue(
      createTaskListResult('GRP-001', 'Booking Request', { page_num: 2, page_size: 10, total: 21 }),
    )

    const wrapper = await mountWithPlugins(ReservationTaskListView)
    await vi.dynamicImportSettled()

    expect(wrapper.text()).toContain('第 2 / 3 页')
  })

  it('reloads the task list when the selected hotel changes', async () => {
    vi.mocked(service.fetchReservationTaskList)
      .mockResolvedValueOnce(createTaskListResult('GRP-001', 'Booking Request'))
      .mockResolvedValueOnce(createTaskListResult('GRP-002', 'Booking Update'))

    const wrapper = await mountWithPlugins(ReservationTaskListView)
    await vi.dynamicImportSettled()
    const authStore = useAuthStore()

    authStore.setSelectedHotelId('HOTEL-BKK')
    await vi.dynamicImportSettled()

    expect(service.fetchReservationTaskList).toHaveBeenCalledTimes(2)
    expect(service.fetchReservationTaskList).toHaveBeenLastCalledWith(
      expect.objectContaining({
        page_num: 1,
      }),
    )
    expect(wrapper.text()).toContain('Booking Update')
  })

  it('reloads order detail when the selected hotel changes', async () => {
    vi.mocked(service.fetchReservationOrderDetail)
      .mockResolvedValueOnce(createOrderDetailResult('GRP-001'))
      .mockResolvedValueOnce(createOrderDetailResult('GRP-002'))

    const wrapper = await mountWithPlugins(ReservationOrderDetailView, '/reservation/orders/20001', {
      ReservationTaskDetailPanel: {
        template: '<section />',
      },
    })
    await vi.dynamicImportSettled()
    const authStore = useAuthStore()

    authStore.setSelectedHotelId('HOTEL-BKK')
    await vi.dynamicImportSettled()

    expect(service.fetchReservationOrderDetail).toHaveBeenCalledTimes(2)
    expect(service.fetchReservationOrderDetail).toHaveBeenLastCalledWith('20001')
    expect(wrapper.text()).toContain('GRP-002')
  })

  it('keeps the latest task list response when filters change quickly', async () => {
    const firstRequest = createDeferred<Awaited<ReturnType<typeof service.fetchReservationTaskList>>>()
    const secondRequest = createDeferred<Awaited<ReturnType<typeof service.fetchReservationTaskList>>>()
    vi.mocked(service.fetchReservationTaskList)
      .mockReturnValueOnce(firstRequest.promise)
      .mockReturnValueOnce(secondRequest.promise)

    const wrapper = await mountWithPlugins(ReservationTaskListView)
    await wrapper.find('input[type="search"]').setValue('fresh')
    expect(service.fetchReservationTaskList).toHaveBeenCalledTimes(2)

    secondRequest.resolve(createTaskListResult('GRP-NEW', 'New Booking Request'))
    await flushPromises()
    expect(wrapper.text()).toContain('GRP-NEW')

    firstRequest.resolve(createTaskListResult('GRP-OLD', 'Old Booking Request'))
    await flushPromises()
    expect(wrapper.text()).toContain('GRP-NEW')
    expect(wrapper.text()).not.toContain('GRP-OLD')
  })

  it('renders full source message conversation returned by the backend', async () => {
    vi.mocked(service.fetchSourceMessageConversation).mockResolvedValue(createSourceMessageConversationResult())

    const wrapper = await mountWithPlugins(
      ReservationSourceMessageConversationView,
      '/reservation/source-messages/30001/conversation',
    )
    await vi.dynamicImportSettled()

    expect(service.fetchSourceMessageConversation).toHaveBeenCalledWith('30001')
    expect(wrapper.text()).toContain('Booking Request')
    expect(wrapper.text()).toContain('清洗后邮件正文')
    expect(wrapper.text()).toContain('rooming-list.xlsx')
    expect(wrapper.find('.message-body').html()).toContain('<strong>清洗后邮件正文</strong>')
    expect(wrapper.find('.message-body').html()).not.toContain('<script>')
    expect(wrapper.find('.message-body').html()).not.toContain('onclick')
    expect(wrapper.text()).not.toContain('原始邮件正文')
    expect(wrapper.text()).not.toContain('接口待接入')
  })

  it('falls back to text when sanitized html is unavailable', async () => {
    const conversationResult = createSourceMessageConversationResult()
    conversationResult.messages[0]!.html_body = '<strong>原始 HTML 不上屏</strong>'
    conversationResult.messages[0]!.html_body_sanitized = null
    conversationResult.messages[0]!.html_render_mode = 'TEXT_ONLY'
    conversationResult.messages[0]!.text_body = '纯文本回退正文'
    vi.mocked(service.fetchSourceMessageConversation).mockResolvedValue(conversationResult)

    const wrapper = await mountWithPlugins(
      ReservationSourceMessageConversationView,
      '/reservation/source-messages/30001/conversation',
    )
    await vi.dynamicImportSettled()

    expect(wrapper.text()).toContain('纯文本回退正文')
    expect(wrapper.text()).not.toContain('原始 HTML 不上屏')
    expect(wrapper.find('.message-body--html').exists()).toBe(false)
  })

  it('does not expose unsafe attachment URLs as links', async () => {
    const conversationResult = createSourceMessageConversationResult()
    conversationResult.messages[0]!.attachments = [
      createAttachment({
        fileName: 'unsafe.html',
        externalUrl: 'javascript:alert(1)',
        externalMediaId: 'unsafe-media',
      }),
    ]
    vi.mocked(service.fetchSourceMessageConversation).mockResolvedValue(conversationResult)

    const wrapper = await mountWithPlugins(
      ReservationSourceMessageConversationView,
      '/reservation/source-messages/30001/conversation',
    )
    await vi.dynamicImportSettled()

    expect(wrapper.text()).toContain('unsafe.html')
    const unsafeLink = wrapper.findAll('a').find((link) => link.text().includes('unsafe.html'))
    expect(unsafeLink).toBeUndefined()
  })

  it('does not warn about duplicate attachment keys when unnamed media changes', async () => {
    const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
    const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined)
    const firstResult = createSourceMessageConversationResult()
    firstResult.messages[0]!.attachments = [
      createAttachment({
        fileName: null,
        externalUrl: null,
        externalMediaId: null,
      }),
    ]
    const secondResult = createSourceMessageConversationResult()
    secondResult.messages[0]!.attachments = [
      createAttachment({
        fileName: null,
        externalUrl: null,
        externalMediaId: null,
      }),
      createAttachment({
        fileName: null,
        externalUrl: null,
        externalMediaId: null,
      }),
    ]
    vi.mocked(service.fetchSourceMessageConversation)
      .mockResolvedValueOnce(firstResult)
      .mockResolvedValueOnce(secondResult)

    const wrapper = await mountWithPlugins(
      ReservationSourceMessageConversationView,
      '/reservation/source-messages/30001/conversation',
    )
    await vi.dynamicImportSettled()
    await wrapper.vm.$router.push('/reservation/source-messages/30002/conversation')
    await vi.dynamicImportSettled()

    expect(wrapper.text()).toContain('未命名附件')
    expect([...consoleWarnSpy.mock.calls, ...consoleErrorSpy.mock.calls].flat().join('\n')).not.toContain(
      'Duplicate keys',
    )
    consoleWarnSpy.mockRestore()
    consoleErrorSpy.mockRestore()
  })
})
