/* @vitest-environment happy-dom */

import type { VueWrapper } from '@vue/test-utils';

import { mount } from '@vue/test-utils';
import { defineComponent, h, nextTick, onMounted } from 'vue';

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import GlobalOfficeDocumentModal from '../index.vue';

const mocks = vi.hoisted(() => ({
  afterEach: vi.fn(),
  autoEditorReady: true,
  closeModal: vi.fn(),
  confirm: vi.fn(),
  confirmDestroy: vi.fn(),
  getOnlyOfficeConfig: vi.fn(),
  getOnlyOfficeSaveStatus: vi.fn(),
  grabEditorFocus: vi.fn(),
  messageError: vi.fn(),
  messageSuccess: vi.fn(),
  messageWarning: vi.fn(),
  openModal: vi.fn(),
  printDmsDocument: vi.fn(),
  printManagedDmsDocument: vi.fn(),
  prepareDmsPrintAttempt: vi.fn(),
  finishDmsPrintAttempt: vi.fn(),
  register: vi.fn(),
  removeRouteHook: vi.fn(),
  requestEditorClose: vi.fn(),
  shouldEditorClose: true,
}));

vi.mock('#/router', () => ({ router: { afterEach: mocks.afterEach, push: vi.fn() } }));

vi.mock('@jnpf/hooks', () => ({
  useGlobSetting: () => ({ apiURL: '' }),
  useMessage: () => ({
    createConfirm: mocks.confirm,
    createMessage: { error: mocks.messageError, success: mocks.messageSuccess, warning: mocks.messageWarning },
  }),
}));

vi.mock('@jnpf/ui/modal', async () => {
  const { defineComponent, h, onMounted } = await import('vue');

  return {
    BasicModal: defineComponent({
      name: 'BasicModal',
      props: { closeFunc: Function },
      emits: ['register'],
      setup(_, { emit, slots }) {
        onMounted(() => emit('register'));

        return () =>
          h('section', { 'data-test': 'basic-modal' }, [
            h('header', { 'data-test': 'modal-title' }, slots.title?.()),
            h('main', { 'data-test': 'modal-content' }, slots.default?.()),
          ]);
      },
    }),
    useModal: () => [mocks.register, { closeModal: mocks.closeModal, openModal: mocks.openModal }],
  };
});

vi.mock('ant-design-vue', async () => {
  const { defineComponent, h } = await import('vue');

  return {
    Button: defineComponent({
      name: 'AButton',
      emits: ['click'],
      setup(_, { emit, slots }) {
        return () => h('button', { type: 'button', onClick: () => emit('click') }, slots.default?.());
      },
    }),
    InputNumber: defineComponent({
      props: ['value', 'disabled'],
      emits: ['update:value'],
      setup(props, { emit }) {
        return () => h('input', { value: props.value, disabled: props.disabled, onInput: (event: any) => emit('update:value', Number(event.target.value)) });
      },
    }),
    Space: defineComponent({
      name: 'ASpace',
      setup(_, { slots }) {
        return () => h('div', slots.default?.());
      },
    }),
    Spin: defineComponent({
      name: 'ASpin',
      props: { tip: String },
      setup(props) {
        return () => h('div', { 'data-test': 'spin' }, props.tip);
      },
    }),
    Tag: defineComponent({
      name: 'ATag',
      setup(_, { slots }) {
        return () => h('span', { 'data-test': 'tag' }, slots.default?.());
      },
    }),
    Tooltip: defineComponent({
      name: 'ATooltip',
      props: { placement: String, title: String },
      setup(_, { slots }) {
        return () => slots.default?.();
      },
    }),
  };
});

vi.mock('#/api/x/onlyoffice', () => ({
  getOnlyOfficeConfig: mocks.getOnlyOfficeConfig,
  getOnlyOfficeSaveStatus: mocks.getOnlyOfficeSaveStatus,
}));

vi.mock('#/views/x/dms/shared/dmsHiprint', () => ({
  DmsPrintError: class DmsPrintError extends Error {},
  printDmsDocument: mocks.printDmsDocument,
  printManagedDmsDocument: mocks.printManagedDmsDocument,
}));

vi.mock('#/views/x/dms/shared/dmsPrintAttempt', () => ({
  prepareDmsPrintAttempt: mocks.prepareDmsPrintAttempt,
  finishDmsPrintAttempt: mocks.finishDmsPrintAttempt,
}));

// 真实编辑器会去加载 DS 的 api.js，测试里换成能手动触发事件的桩
vi.mock('#/components/OnlyOfficeEditor/src/Editor.vue', () => ({
  default: defineComponent({
    name: 'OnlyOfficeEditor',
    props: { config: { required: true, type: Object } },
    emits: ['close', 'error', 'modified', 'ready'],
    setup(_, { emit, expose }) {
      onMounted(() => {
        if (mocks.autoEditorReady) emit('ready');
      });
      expose({
        grabFocus: mocks.grabEditorFocus,
        requestClose() {
          mocks.requestEditorClose();
          if (mocks.shouldEditorClose) emit('close');
        },
      });
      return () =>
        h('div', {
          'data-test': 'onlyoffice-editor',
          onClick: () => emit('modified', true),
          onContextmenu: () => {
            emit('modified', true);
            emit('modified', false);
          },
          onDblclick: () => {
            emit('modified', true);
            emit('modified', false);
          },
          onMouseup: () => emit('ready'),
        });
    },
  }),
}));

const { onlineUtils } = await import('#/utils/jnpf');

const FILE = { fileId: 'file_001', name: '检验报告.docx' };
const CONFIG_RESPONSE = {
  data: {
    document: { fileType: 'docx', key: 'key_v1', title: '检验报告.docx', url: 'http://gw/api/file/Image/annex/file_001?s=sk' },
    documentType: 'word',
    editorConfig: { callbackUrl: 'http://gw/api/biz/onlyoffice/callback', mode: 'edit' },
    token: 'signed.jwt.value',
  },
};

let wrapper: VueWrapper<any>;
/** 组件注册进 router.afterEach 的那个钩子，测试里手动触发以模拟路由跳转 */
let routeHook: (to: any, from: any, failure?: any) => void = () => {};

async function flush(times = 3) {
  for (let i = 0; i < times; i++) {
    await nextTick();
  }
}

describe('globalOfficeDocumentModal', () => {
  beforeEach(() => {
    mocks.openModal.mockClear();
    mocks.autoEditorReady = true;
    mocks.closeModal.mockClear();
    mocks.confirm.mockClear();
    mocks.confirm.mockImplementation(() => ({ destroy: mocks.confirmDestroy }));
    mocks.confirmDestroy.mockClear();
    mocks.removeRouteHook.mockClear();
    mocks.getOnlyOfficeConfig.mockReset();
    mocks.getOnlyOfficeConfig.mockResolvedValue(CONFIG_RESPONSE);
    mocks.getOnlyOfficeSaveStatus.mockReset();
    mocks.getOnlyOfficeSaveStatus.mockResolvedValue({ data: { lastSaveTime: null, sessionStatus: 'IDLE', version: 1 } });
    mocks.messageSuccess.mockClear();
    mocks.messageError.mockClear();
    mocks.messageWarning.mockClear();
    mocks.printManagedDmsDocument.mockReset();
    mocks.prepareDmsPrintAttempt.mockReset();
    mocks.finishDmsPrintAttempt.mockReset();
    mocks.printDmsDocument.mockReset();
    mocks.printDmsDocument.mockResolvedValue({ mode: 'hiprint' });
    mocks.requestEditorClose.mockClear();
    mocks.grabEditorFocus.mockClear();
    mocks.shouldEditorClose = true;
    mocks.afterEach.mockReset();
    mocks.afterEach.mockImplementation((hook) => {
      routeHook = hook;
      return mocks.removeRouteHook;
    });
    wrapper = mount(GlobalOfficeDocumentModal);
  });

  afterEach(() => {
    wrapper?.unmount();
  });

  it('重打预览打开不派发，手动发送成功才刷新关闭，固定一份', async () => {
    const send = vi.fn().mockResolvedValue(undefined);
    const refresh = vi.fn();
    onlineUtils.openOfficeDocument({ file: FILE, fileType: 'dms', mode: 'view', dmsPrint: {
      fileVersionId: 'version', copies: 1, onPrintSuccess: refresh,
      reprint: { requestId: 'request', printRecordId: 'record', send },
    } });
    await flush();
    expect(send).not.toHaveBeenCalled();
    expect(wrapper.text()).toContain('重打份数：1 份');
    expect(wrapper.find('[data-test="dms-print-icon"]').exists()).toBe(true);
    expect(wrapper.find('[aria-label="关闭"]').exists()).toBe(true);
    expect(wrapper.find('#dms-print-copies').exists()).toBe(false);
    await wrapper.find('[aria-label="打印"]').trigger('click');
    await flush(6);
    expect(send).toHaveBeenCalledTimes(1);
    expect(refresh).toHaveBeenCalledTimes(1);
    expect(mocks.closeModal).toHaveBeenCalledTimes(1);
    expect(mocks.printDmsDocument).not.toHaveBeenCalled();
    expect(mocks.prepareDmsPrintAttempt).not.toHaveBeenCalled();
  });
  it('重打预览直接关闭不派发，不更新执行状态', async () => {
    const send = vi.fn(); const refresh = vi.fn();
    onlineUtils.openOfficeDocument({ file: FILE, fileType: 'dms', mode: 'view', dmsPrint: {
      fileVersionId: 'version', onPrintSuccess: refresh, reprint: { requestId: 'request', printRecordId: 'record', send },
    } });
    await flush();
    await wrapper.find('[aria-label="关闭"]').trigger('click');
    await flush();
    expect(send).not.toHaveBeenCalled(); expect(refresh).not.toHaveBeenCalled();
    expect(mocks.closeModal).toHaveBeenCalledTimes(1);
  });
  it('重打超时停止loading，迟到成功通知仍关闭且只刷新一次', async () => {
    const send = vi.fn().mockRejectedValue(new Error('发送结果未知')); const refresh = vi.fn();
    onlineUtils.openOfficeDocument({ file: FILE, fileType: 'dms', mode: 'view', dmsPrint: {
      fileVersionId: 'version', onPrintSuccess: refresh, reprint: { requestId: 'request', printRecordId: 'record', send },
    } });
    await flush();
    await wrapper.find('[aria-label="打印"]').trigger('click');
    await flush(6);
    expect(wrapper.find('[data-test="dms-print-loading-icon"]').exists()).toBe(false);
    expect(mocks.closeModal).not.toHaveBeenCalled();
    onlineUtils.getEmitter().emit('DMS_REPRINT_COMPLETED', { printRecordId: 'other', requestId: 'request' });
    await flush(); expect(refresh).not.toHaveBeenCalled();
    onlineUtils.getEmitter().emit('DMS_REPRINT_COMPLETED', { printRecordId: 'record', requestId: 'request' });
    onlineUtils.getEmitter().emit('DMS_REPRINT_COMPLETED', { printRecordId: 'record', requestId: 'request' });
    await flush(6);
    expect(refresh).toHaveBeenCalledTimes(1); expect(mocks.closeModal).toHaveBeenCalledTimes(1);
  });

  it('拿到后端 config 后才渲染编辑器，且透传文档参数和业务场景', async () => {
    onlineUtils.openOfficeDocument({ file: FILE, bizScene: 'eln.template.annotate' });
    await flush();

    expect(mocks.openModal).toHaveBeenCalledWith(true);
    expect(mocks.getOnlyOfficeConfig).toHaveBeenCalledWith(
      expect.objectContaining({ bizScene: 'eln.template.annotate', fileId: 'file_001', fileName: '检验报告.docx', fileType: 'annex', mode: 'edit' }),
    );
    expect(mocks.getOnlyOfficeSaveStatus).toHaveBeenCalledWith('file_001', 'annex');
    expect(wrapper.find('[data-test="onlyoffice-editor"]').exists()).toBe(true);
  });

  it('弹窗打开失败时提示并通知调用方，之后可以重试', async () => {
    const error = new Error('useModal instance is undefined!');
    const onOpenError = vi.fn();
    mocks.openModal.mockImplementationOnce(() => {
      throw error;
    });
    onlineUtils.openOfficeDocument({ file: FILE, onOpenError });
    await flush();
    expect(mocks.messageError).toHaveBeenCalledWith(error.message);
    expect(onOpenError).toHaveBeenCalledWith(error);
    expect(mocks.getOnlyOfficeConfig).not.toHaveBeenCalled();
    onlineUtils.openOfficeDocument({ file: FILE, mode: 'view' });
    await flush();
    expect(mocks.getOnlyOfficeConfig).toHaveBeenCalled();
  });

  it('关闭预览后可打开上传附件编辑，并显示配置接口错误', async () => {
    mocks.getOnlyOfficeConfig.mockResolvedValueOnce({ data: { ...CONFIG_RESPONSE.data, editorConfig: { mode: 'view' } } });
    onlineUtils.openOfficeDocument({ file: FILE, mode: 'view' });
    await flush();
    await wrapper.find('button[aria-label="关闭"]').trigger('click');
    await flush();
    mocks.getOnlyOfficeConfig.mockRejectedValueOnce(new Error('系统异常'));
    onlineUtils.openOfficeDocument({ file: { ...FILE, fileId: 'lease' }, bizScene: 'visualdev.uploadFile', mode: 'edit' });
    await flush(6);
    expect(mocks.openModal).toHaveBeenCalledTimes(2);
    expect(wrapper.find('.state-pane').text()).toBe('系统异常');
  });

  it('DMS 预览把文件类型和短期票据交给配置签发接口', async () => {
    onlineUtils.openOfficeDocument({
      dmsAccessTicket: 'ticket-1',
      file: { fileId: 'version-1', name: '质量手册.docx' },
      fileType: 'dms',
      mode: 'view',
    });
    await flush();

    expect(mocks.getOnlyOfficeConfig).toHaveBeenCalledWith(
      expect.objectContaining({
        dmsAccessTicket: 'ticket-1',
        fileId: 'version-1',
        fileName: '质量手册.docx',
        fileType: 'dms',
        mode: 'view',
      }),
    );
  });

  it('DMS 票据失效时静默换票并继续打开预览', async () => {
    mocks.getOnlyOfficeConfig.mockRejectedValueOnce(new Error('DMS 预览票据无效或已过期'));
    const refresh = vi.fn().mockResolvedValue({ accessTicket: 'fresh-ticket', fileVersionId: 'version-1', fileName: '质量手册.docx' });
    onlineUtils.openOfficeDocument({
      dmsAccessTicket: 'expired-ticket',
      refreshDmsPreviewTicket: refresh,
      file: { fileId: 'version-1', name: '质量手册.docx' },
      fileType: 'dms',
      mode: 'view',
    });
    await flush(12);
    expect(refresh).toHaveBeenCalledTimes(1);
    expect(mocks.getOnlyOfficeConfig).toHaveBeenLastCalledWith(expect.objectContaining({ dmsAccessTicket: 'fresh-ticket', fileId: 'version-1' }), {
      errorMessageMode: 'none',
    });
    expect(wrapper.find('[data-test="onlyoffice-editor"]').exists()).toBe(true);
    expect(mocks.messageError).not.toHaveBeenCalled();
  });

  it('仅为携带打印上下文的 DMS 预览显示打印按钮', async () => {
    onlineUtils.openOfficeDocument({ file: FILE, mode: 'view' });
    await flush();
    expect(wrapper.find('[aria-label="打印"]').exists()).toBe(false);

    onlineUtils.openOfficeDocument({
      dmsPrint: { copies: 1, fileVersionId: 'version-1', pageVariables: [] },
      file: { fileId: 'version-1', name: '质量手册.docx' },
      fileType: 'dms',
      mode: 'view',
    });
    await flush();
    expect(wrapper.find('[aria-label="打印"]').exists()).toBe(true);
  });

  it('OnlyOffice 文档未 ready 前禁用打印，ready 后才允许点击', async () => {
    mocks.autoEditorReady = false;
    onlineUtils.openOfficeDocument({
      dmsPrint: { copies: 1, fileVersionId: 'version-1', pageVariables: [] },
      file: { fileId: 'version-1', name: '质量手册.docx' },
      fileType: 'dms',
      mode: 'view',
    });
    await flush();

    const printButton = wrapper.find('[aria-label="打印"]');
    expect(printButton.attributes('disabled')).toBeDefined();
    await printButton.trigger('click');
    expect(mocks.printDmsDocument).not.toHaveBeenCalled();

    await wrapper.find('[data-test="onlyoffice-editor"]').trigger('mouseup');
    await flush();
    expect(wrapper.find('[aria-label="打印"]').attributes('disabled')).toBeUndefined();
  });

  it('Hiprint 离线时不进入打印票据和 PDF 编排链路', async () => {
    const ready = vi.spyOn(onlineUtils, 'isHiprintClientReady').mockResolvedValue(false);
    onlineUtils.openOfficeDocument({
      dmsPrint: { copies: 1, fileVersionId: 'version-1', pageVariables: [] },
      file: { fileId: 'version-1', name: '质量手册.docx' },
      fileType: 'dms',
      mode: 'view',
    });
    await flush();

    await wrapper.find('[aria-label="打印"]').trigger('click');
    await flush();

    expect(mocks.printDmsDocument).not.toHaveBeenCalled();
    expect(mocks.messageError).toHaveBeenCalledWith('打印客户端未连接，请启动客户端后重试');
    ready.mockRestore();
  });

  it('打印前回调成功后才进入 PDF 编排，回调失败时终止打印', async () => {
    const beforePrint = vi.fn().mockResolvedValueOnce(undefined).mockRejectedValueOnce(new Error('打印确认失败'));
    const ready = vi.spyOn(onlineUtils, 'isHiprintClientReady').mockResolvedValue(true);
    onlineUtils.openOfficeDocument({
      dmsPrint: { beforePrint, copies: 1, fileVersionId: 'version-1', pageVariables: [] },
      file: { fileId: 'version-1', name: '质量手册.docx' },
      fileType: 'dms',
      mode: 'view',
    });
    await flush();

    await wrapper.find('[aria-label="打印"]').trigger('click');
    await flush();
    expect(beforePrint).toHaveBeenCalledTimes(1);
    expect(mocks.printDmsDocument).toHaveBeenCalledTimes(1);

    await wrapper.find('[aria-label="打印"]').trigger('click');
    await flush();
    expect(beforePrint).toHaveBeenCalledTimes(2);
    expect(mocks.printDmsDocument).toHaveBeenCalledTimes(1);
    expect(mocks.messageError).toHaveBeenCalledWith('打印确认失败');
    ready.mockRestore();
  });

  it('打印中和打印完成时原子切换加载与打印图标', async () => {
    let completePrint: (value: unknown) => void = () => {};
    mocks.printDmsDocument.mockImplementation(
      () =>
        new Promise((resolve) => {
          completePrint = resolve;
        }),
    );
    const ready = vi.spyOn(onlineUtils, 'isHiprintClientReady').mockResolvedValue(true);
    onlineUtils.openOfficeDocument({
      dmsPrint: { copies: 1, fileVersionId: 'version-1', pageVariables: [] },
      file: { fileId: 'version-1', name: '质量手册.docx' },
      fileType: 'dms',
      mode: 'view',
    });
    await flush();

    await wrapper.find('[aria-label="打印"]').trigger('click');
    await flush();
    expect(wrapper.find('[data-test="dms-print-loading-icon"]').exists()).toBe(true);
    expect(wrapper.find('[data-test="dms-print-icon"]').exists()).toBe(false);

    completePrint({ mode: 'hiprint' });
    await flush(5);
    expect(wrapper.find('[data-test="dms-print-loading-icon"]').exists()).toBe(false);
    expect(wrapper.find('[data-test="dms-print-icon"]').exists()).toBe(true);
    ready.mockRestore();
  });

  it('所有 Hiprint 批次确认成功后等待业务回调再关闭预览', async () => {
    const order: string[] = [];
    const onPrintSuccess = vi.fn(() => {
      order.push('refresh');
    });
    mocks.closeModal.mockImplementation(() => order.push('close'));
    mocks.printDmsDocument.mockImplementation(async (_fileName, printConfig) => {
      await printConfig.onPrintSuccess?.();
      return { mode: 'hiprint' };
    });
    const ready = vi.spyOn(onlineUtils, 'isHiprintClientReady').mockResolvedValue(true);
    onlineUtils.openOfficeDocument({
      dmsPrint: {
        closeOnPrintSuccess: true,
        copies: 1,
        fileVersionId: 'version-1',
        onPrintSuccess,
        pageVariables: [],
      },
      file: { fileId: 'version-1', name: '质量手册.docx' },
      fileType: 'dms',
      mode: 'view',
    });
    await flush();

    await wrapper.find('[aria-label="打印"]').trigger('click');
    await flush();

    expect(order).toEqual(['refresh', 'close']);
    expect(mocks.closeModal).toHaveBeenCalledTimes(1);
    expect(onPrintSuccess).toHaveBeenCalledTimes(1);
    ready.mockRestore();
  });

  it('成功回传后的确认失败仅重试确认，期间阻止关闭和新预览', async () => {
    const attempt = { id: 'attempt', taskId: 'task', copies: 3, state: 'OPEN' };
    mocks.prepareDmsPrintAttempt.mockResolvedValue(attempt);
    mocks.finishDmsPrintAttempt.mockRejectedValueOnce(new Error('确认网络异常')).mockResolvedValueOnce(undefined);
    const refresh = vi.fn();
    const ready = vi.spyOn(onlineUtils, 'isHiprintClientReady').mockResolvedValue(true);
    mocks.printManagedDmsDocument.mockResolvedValue(undefined);
    onlineUtils.openOfficeDocument({
      dmsPrint: {
        printQuantity: { total: 10, printed: 2 },
        printTaskId: 'task',
        onPrintSuccess: refresh,
        closeOnPrintSuccess: true,
        copies: 1,
        fileVersionId: 'version-1',
      },
      file: { fileId: 'version-1', name: '质量手册.docx' },
      fileType: 'dms',
      mode: 'view',
    });
    await flush();
    expect(wrapper.find('[data-test="print-quantity-summary"]').text()).toContain('剩余份数：8');
    expect((wrapper.find('[aria-label="本次打印份数"]').element as HTMLInputElement).value).toBe('1');
    await wrapper.find('[aria-label="本次打印份数"]').setValue('3');
    await wrapper.find('[aria-label="打印"]').trigger('click');
    await flush(5);
    expect(mocks.finishDmsPrintAttempt).toHaveBeenCalledWith(attempt);
    expect(mocks.printManagedDmsDocument.mock.calls[0]![2].copies).toBe(3);
    expect(wrapper.find('[aria-label="本次打印份数"]').attributes('disabled')).toBeDefined();
    expect(refresh).not.toHaveBeenCalled();
    expect(mocks.closeModal).not.toHaveBeenCalled();
    await wrapper.find('[aria-label="关闭"]').trigger('click');
    onlineUtils.openOfficeDocument({ file: { fileId: 'other', name: 'other.docx' }, mode: 'view' });
    await flush();
    expect(mocks.closeModal).not.toHaveBeenCalled();
    await wrapper.find('[aria-label="重试记录确认"]').trigger('click');
    await flush(5);
    expect(mocks.finishDmsPrintAttempt).toHaveBeenCalledTimes(2);
    expect(mocks.finishDmsPrintAttempt).toHaveBeenLastCalledWith(attempt);
    expect(mocks.printManagedDmsDocument).toHaveBeenCalledTimes(1);
    expect(mocks.prepareDmsPrintAttempt).toHaveBeenCalledTimes(1);
    expect(refresh).toHaveBeenCalledTimes(1);
    expect(mocks.closeModal).toHaveBeenCalledTimes(1);
    ready.mockRestore();
  });

  it('旧台账配置在发送前阻断，不能先打印后被后端拒绝', async () => {
    onlineUtils.openOfficeDocument({
      dmsPrint: { printQuantity: { total: 10, printed: 0 }, confirmPrint: vi.fn(), fileVersionId: 'version-1' },
      file: { fileId: 'version-1', name: '质量手册.docx' },
      fileType: 'dms',
      mode: 'view',
    });
    await flush();
    await wrapper.find('[aria-label="打印"]').trigger('click');
    await flush();
    expect(mocks.printDmsDocument).not.toHaveBeenCalled();
    expect(mocks.messageError).toHaveBeenCalledWith(expect.stringContaining('台账配置需要更新'));
  });

  it('重新打开后恢复原技术任务份数，未知结果只走恢复入口', async () => {
    const attempt = { id: 'original', taskId: 'task', copies: 2, state: 'OPEN' };
    mocks.prepareDmsPrintAttempt.mockResolvedValue(attempt);
    mocks.printManagedDmsDocument.mockRejectedValueOnce(new Error('发送结果未知')).mockResolvedValueOnce(undefined);
    mocks.finishDmsPrintAttempt.mockResolvedValue(undefined);
    onlineUtils.openOfficeDocument({
      dmsPrint: { printTaskId: 'task', printQuantity: { total: 10, printed: 1 }, fileVersionId: 'version-1' },
      file: { fileId: 'version-1', name: '质量手册.docx' },
      fileType: 'dms',
      mode: 'view',
    });
    await flush();
    await wrapper.find('[aria-label="打印"]').trigger('click');
    await flush(5);
    expect((wrapper.find('[aria-label="本次打印份数"]').element as HTMLInputElement).value).toBe('2');
    expect(mocks.finishDmsPrintAttempt).not.toHaveBeenCalled();
    await wrapper.find('[aria-label="恢复打印任务"]').trigger('click');
    await flush(5);
    expect(mocks.prepareDmsPrintAttempt).toHaveBeenCalledTimes(2);
    expect(mocks.printManagedDmsDocument.mock.calls[1]![2].id).toBe('original');
    expect(mocks.finishDmsPrintAttempt).toHaveBeenCalledTimes(1);
    expect(mocks.printDmsDocument).not.toHaveBeenCalled();
  });

  it('异步确认未完成前不刷新、不关闭且保持忙碌', async () => {
    let resolveConfirmation!: () => void;
    const confirmPrint = vi.fn(
      () =>
        new Promise<void>((resolve) => {
          resolveConfirmation = resolve;
        }),
    );
    const refresh = vi.fn();
    const ready = vi.spyOn(onlineUtils, 'isHiprintClientReady').mockResolvedValue(true);
    mocks.printDmsDocument.mockImplementation(async (_name, options) => {
      await options.onPrintSuccess();
    });
    onlineUtils.openOfficeDocument({
      dmsPrint: { confirmPrint, onPrintSuccess: refresh, closeOnPrintSuccess: true, copies: 1, fileVersionId: 'version-1' },
      file: { fileId: 'version-1', name: '质量手册.docx' },
      fileType: 'dms',
      mode: 'view',
    });
    await flush();
    await wrapper.find('[aria-label="打印"]').trigger('click');
    await flush();
    expect(confirmPrint).toHaveBeenCalledTimes(1);
    expect(refresh).not.toHaveBeenCalled();
    expect(mocks.closeModal).not.toHaveBeenCalled();
    expect(wrapper.find('[aria-label="正在打印"]').exists()).toBe(true);
    resolveConfirmation();
    await flush(5);
    expect(refresh).toHaveBeenCalledTimes(1);
    expect(mocks.closeModal).toHaveBeenCalledTimes(1);
    ready.mockRestore();
  });

  it('打印失败时不关闭预览且不执行成功回调', async () => {
    const onPrintSuccess = vi.fn();
    mocks.printDmsDocument.mockRejectedValue(new Error('打印失败'));
    const ready = vi.spyOn(onlineUtils, 'isHiprintClientReady').mockResolvedValue(true);
    onlineUtils.openOfficeDocument({
      dmsPrint: {
        closeOnPrintSuccess: true,
        copies: 1,
        fileVersionId: 'version-1',
        onPrintSuccess,
        pageVariables: [],
      },
      file: { fileId: 'version-1', name: '质量手册.docx' },
      fileType: 'dms',
      mode: 'view',
    });
    await flush();

    await wrapper.find('[aria-label="打印"]').trigger('click');
    await flush();

    expect(mocks.closeModal).not.toHaveBeenCalled();
    expect(onPrintSuccess).not.toHaveBeenCalled();
    expect(wrapper.find('[data-test="dms-print-loading-icon"]').exists()).toBe(false);
    expect(wrapper.find('[data-test="dms-print-icon"]').exists()).toBe(true);
    expect(mocks.messageError).toHaveBeenCalledWith('打印失败');
    ready.mockRestore();
  });

  it('批注预览不查询保存状态，审批模式沿用保存基线', async () => {
    onlineUtils.openOfficeDocument({ file: FILE, mode: 'viewComments' });
    await flush();
    expect(mocks.getOnlyOfficeSaveStatus).not.toHaveBeenCalled();
    expect(mocks.getOnlyOfficeConfig).toHaveBeenCalledWith(expect.objectContaining({ mode: 'viewComments' }));
    onlineUtils.openOfficeDocument({ file: FILE, mode: 'review' });
    await flush();
    expect(mocks.getOnlyOfficeSaveStatus).toHaveBeenCalledWith('file_001', 'annex');
    expect(mocks.getOnlyOfficeConfig).toHaveBeenCalledWith(expect.objectContaining({ mode: 'review' }));
  });

  it('签批页模式按可编辑模式获取配置与保存状态', async () => {
    onlineUtils.openOfficeDocument({ file: FILE, mode: 'qianpiye' });
    await flush();

    expect(mocks.getOnlyOfficeSaveStatus).toHaveBeenCalledWith('file_001', 'annex');
    expect(mocks.getOnlyOfficeConfig).toHaveBeenCalledWith(
      expect.objectContaining({ bizModule: 'dms', bizScene: 'dms.qianpiye', fileId: 'file_001', mode: 'qianpiye' }),
    );
  });

  it('config 和状态查询使用同一文件存储类型', async () => {
    onlineUtils.openOfficeDocument({ file: FILE, fileType: 'temporary' });
    await flush();

    expect(mocks.getOnlyOfficeConfig).toHaveBeenCalledWith(expect.objectContaining({ fileType: 'temporary' }));
    expect(mocks.getOnlyOfficeSaveStatus).toHaveBeenCalledWith('file_001', 'temporary');
  });

  it('编辑器挂载前必须取得保存基线，避免首次 status 6 被误当成基线', async () => {
    mocks.getOnlyOfficeSaveStatus.mockRejectedValueOnce(new Error('save-status unavailable'));

    onlineUtils.openOfficeDocument({ file: FILE });
    await flush();

    expect(mocks.getOnlyOfficeConfig).not.toHaveBeenCalled();
    expect(wrapper.find('[data-test="onlyoffice-editor"]').exists()).toBe(false);
    expect(wrapper.find('[data-test="modal-content"]').text()).toContain('save-status unavailable');
  });

  it('普通标注不发送 repairBindingId，修复时准确透传绑定 ID', async () => {
    onlineUtils.openOfficeDocument({ file: FILE, bizScene: 'eln.template.annotate' });
    await flush();

    expect(mocks.getOnlyOfficeConfig).toHaveBeenLastCalledWith(expect.not.objectContaining({ repairBindingId: expect.anything() }));

    onlineUtils.openOfficeDocument({ file: FILE, bizScene: 'eln.template.annotate', repairBindingId: 'binding-1' });
    await flush();

    expect(mocks.getOnlyOfficeConfig).toHaveBeenLastCalledWith(expect.objectContaining({ repairBindingId: 'binding-1' }));
  });

  it('普通打开不发送 locateControlTag，体检定位时准确透传控件 Tag', async () => {
    onlineUtils.openOfficeDocument({ file: FILE, bizScene: 'eln.template.annotate' });
    await flush();
    expect(mocks.getOnlyOfficeConfig).toHaveBeenLastCalledWith(expect.not.objectContaining({ locateControlTag: expect.anything() }));

    onlineUtils.openOfficeDocument({ file: FILE, bizScene: 'eln.template.annotate', locateControlTag: 'eln.field.orphan', mode: 'view' });
    await flush();
    expect(mocks.getOnlyOfficeConfig).toHaveBeenLastCalledWith(expect.objectContaining({ locateControlTag: 'eln.field.orphan', mode: 'view' }));
  });

  it('后端把 edit 降级为 view 时给出只读提示', async () => {
    mocks.getOnlyOfficeConfig.mockResolvedValue({
      data: { ...CONFIG_RESPONSE.data, editorConfig: { ...CONFIG_RESPONSE.data.editorConfig, mode: 'view' } },
    });

    onlineUtils.openOfficeDocument({ file: FILE, mode: 'edit' });
    await flush();

    // 标签紧跟标题，不在右侧按钮区——放右边太容易被当成装饰略过
    expect(wrapper.find('.header-main [data-test="tag"]').exists()).toBe(true);
  });

  it('config 缺 token 时不渲染编辑器——没有签名 DS 一定会拒，早报错好过让编辑器空转', async () => {
    mocks.getOnlyOfficeConfig.mockResolvedValue({ data: { document: { key: 'key_v1' } } });

    onlineUtils.openOfficeDocument({ file: FILE });
    await flush();

    expect(wrapper.find('[data-test="onlyoffice-editor"]').exists()).toBe(false);
    expect(wrapper.text()).toContain('编辑器配置不完整');
  });

  it('改动过再关闭才触发 onSave，onClose 则总会触发', async () => {
    const onSave = vi.fn();
    const onClose = vi.fn();

    onlineUtils.openOfficeDocument({ file: FILE, onClose, onSave });
    await flush();

    await wrapper.find('[data-test="onlyoffice-editor"]').trigger('click'); // 桩组件的 click = emit modified
    await wrapper.find('button').trigger('click'); // 头部关闭按钮
    await flush();

    expect(mocks.confirm).toHaveBeenCalledWith(
      expect.objectContaining({
        cancelText: '继续编辑',
        content: '关闭后，当前未保存的文档更改将丢失。',
        okText: '放弃并关闭',
        okType: 'danger',
        title: '放弃未保存的更改？',
      }),
    );
    expect(mocks.requestEditorClose).not.toHaveBeenCalled();
    mocks.confirm.mock.calls[0]?.[0].onOk();
    await flush();

    expect(mocks.requestEditorClose).not.toHaveBeenCalled();
    expect(onSave).toHaveBeenCalledWith({ file: FILE, modified: true });
    expect(onClose).toHaveBeenCalledTimes(1);
    expect(mocks.closeModal).toHaveBeenCalled();
  });

  it('提交收尾委托编辑器关闭并等待服务端释放，不走放弃修改分支', async () => {
    const registerSubmitPreparation = vi.fn();
    const onClose = vi.fn();
    onlineUtils.openOfficeDocument({ file: FILE, onClose, registerSubmitPreparation });
    await flush();
    await wrapper.find('[data-test="onlyoffice-editor"]').trigger('click');
    let release!: (value: unknown) => void;
    mocks.getOnlyOfficeSaveStatus.mockImplementationOnce(() => new Promise((resolve) => { release = resolve; }));
    const prepare = registerSubmitPreparation.mock.calls[0]![0];
    let finished = false;
    const pending = prepare().then(() => { finished = true; });
    await flush();
    expect(mocks.requestEditorClose).toHaveBeenCalledOnce();
    expect(mocks.confirm).not.toHaveBeenCalled();
    expect(finished).toBe(false);
    release({ data: { sessionStatus: 'IDLE', lastSaveTime: 200 } });
    await pending;
    expect(onClose).toHaveBeenCalledOnce();
    expect(registerSubmitPreparation).toHaveBeenLastCalledWith(undefined);
  });

  it('没改动就关闭不触发 onSave', async () => {
    const onSave = vi.fn();

    onlineUtils.openOfficeDocument({ file: FILE, onSave });
    await flush();
    await wrapper.find('button').trigger('click');
    await flush();

    expect(onSave).not.toHaveBeenCalled();
  });

  it('编辑器关闭后等待后端会话空闲才完成外层关闭', async () => {
    const onClose = vi.fn();
    let resolveReleaseStatus: (value: any) => void = () => {};
    mocks.getOnlyOfficeSaveStatus
      .mockResolvedValueOnce({ data: { lastSaveTime: null, sessionStatus: 'IDLE', version: 1 } })
      .mockReturnValueOnce(new Promise((resolve) => (resolveReleaseStatus = resolve)));

    onlineUtils.openOfficeDocument({ file: FILE, onClose });
    await flush();
    await wrapper.find('button').trigger('click');
    await flush();

    expect(mocks.requestEditorClose).toHaveBeenCalledTimes(1);
    expect(wrapper.find('[data-test="onlyoffice-editor"]').exists()).toBe(false);
    expect(wrapper.find('[data-test="document-saving-mask"]').text()).toContain('正在关闭文档');
    expect(mocks.closeModal).not.toHaveBeenCalled();
    expect(onClose).not.toHaveBeenCalled();

    resolveReleaseStatus({ data: { lastSaveTime: null, sessionStatus: 'IDLE', version: 2 } });
    await flush();

    expect(mocks.closeModal).toHaveBeenCalledTimes(1);
    expect(onClose).toHaveBeenCalledTimes(1);
  });

  it('等待会话释放期间路由切换也不能提前完成关闭', async () => {
    let resolveReleaseStatus: (value: any) => void = () => {};
    mocks.getOnlyOfficeSaveStatus
      .mockResolvedValueOnce({ data: { lastSaveTime: null, sessionStatus: 'IDLE', version: 1 } })
      .mockReturnValueOnce(new Promise((resolve) => (resolveReleaseStatus = resolve)));

    onlineUtils.openOfficeDocument({ file: FILE });
    await flush();
    await wrapper.find('button').trigger('click');
    await flush();

    routeHook({ fullPath: '/b' }, { fullPath: '/a' });
    await flush();

    expect(mocks.closeModal).not.toHaveBeenCalled();

    resolveReleaseStatus({ data: { lastSaveTime: null, sessionStatus: 'IDLE', version: 2 } });
    await flush();

    expect(mocks.closeModal).toHaveBeenCalledTimes(1);
  });

  it('会话释放超时不强制关闭并允许用户重试', async () => {
    vi.useFakeTimers();
    mocks.getOnlyOfficeSaveStatus
      .mockResolvedValueOnce({ data: { lastSaveTime: null, sessionStatus: 'IDLE', version: 1 } })
      .mockResolvedValue({ data: { lastSaveTime: null, sessionStatus: 'EDITING', version: 1 } });

    onlineUtils.openOfficeDocument({ file: FILE });
    await flush();
    await wrapper.find('button').trigger('click');
    await flush();
    await vi.advanceTimersByTimeAsync(60_000);
    await flush();

    expect(mocks.closeModal).not.toHaveBeenCalled();
    expect(mocks.messageError).toHaveBeenCalledWith('文档会话尚未结束，请稍后重试关闭');

    mocks.getOnlyOfficeSaveStatus.mockResolvedValueOnce({ data: { lastSaveTime: null, sessionStatus: 'IDLE', version: 2 } });
    await wrapper.find('button').trigger('click');
    await flush();

    expect(mocks.closeModal).toHaveBeenCalledTimes(1);
    vi.useRealTimers();
  });

  it('选择继续编辑后等确认框退场再恢复文档焦点，保留原编辑器', async () => {
    const onClose = vi.fn();

    onlineUtils.openOfficeDocument({ file: FILE, onClose });
    await flush();
    await wrapper.find('[data-test="onlyoffice-editor"]').trigger('click');
    await wrapper.find('button').trigger('click');
    await flush();

    expect(mocks.confirm).toHaveBeenCalledTimes(1);
    expect(mocks.requestEditorClose).not.toHaveBeenCalled();
    const confirm = mocks.confirm.mock.calls[0]?.[0];
    expect(confirm.focusTriggerAfterClose).toBe(false);
    confirm.onCancel();
    await flush();

    expect(mocks.grabEditorFocus).not.toHaveBeenCalled();
    await confirm.afterClose();
    expect(mocks.grabEditorFocus).toHaveBeenCalledTimes(1);

    expect(mocks.closeModal).not.toHaveBeenCalled();
    expect(onClose).not.toHaveBeenCalled();
    expect(wrapper.find('[data-test="onlyoffice-editor"]').exists()).toBe(true);
  });

  it('确认放弃并关闭时不恢复文档焦点', async () => {
    onlineUtils.openOfficeDocument({ file: FILE });
    await flush();
    await wrapper.find('[data-test="onlyoffice-editor"]').trigger('click');
    await wrapper.find('button').trigger('click');
    const confirm = mocks.confirm.mock.calls[0]?.[0];

    await confirm.onOk();
    await confirm.afterClose();

    expect(mocks.grabEditorFocus).not.toHaveBeenCalled();
    expect(mocks.closeModal).toHaveBeenCalledTimes(1);
  });

  it('继续编辑后打开另一文档，旧确认框退场不能抢新文档焦点', async () => {
    onlineUtils.openOfficeDocument({ file: FILE });
    await flush();
    await wrapper.find('[data-test="onlyoffice-editor"]').trigger('click');
    await wrapper.find('button').trigger('click');
    const confirm = mocks.confirm.mock.calls[0]?.[0];
    confirm.onCancel();

    onlineUtils.openOfficeDocument({ file: { fileId: 'file_002', name: '另一文档.docx' } });
    await flush();
    await confirm.afterClose();

    expect(mocks.grabEditorFocus).not.toHaveBeenCalled();
  });

  it('连续弹出关闭确认时，旧确认框退场不能抢当前确认框焦点', async () => {
    onlineUtils.openOfficeDocument({ file: FILE });
    await flush();
    await wrapper.find('[data-test="onlyoffice-editor"]').trigger('click');
    await wrapper.find('button').trigger('click');
    const firstConfirm = mocks.confirm.mock.calls[0]?.[0];
    firstConfirm.onCancel();
    await wrapper.find('button').trigger('click');
    await firstConfirm.afterClose();
    expect(mocks.grabEditorFocus).not.toHaveBeenCalled();

    const secondConfirm = mocks.confirm.mock.calls[1]?.[0];
    secondConfirm.onCancel();
    await secondConfirm.afterClose();
    expect(mocks.grabEditorFocus).toHaveBeenCalledTimes(1);
  });

  it('未保存确认框打开期间忽略重复关闭请求', async () => {
    onlineUtils.openOfficeDocument({ file: FILE });
    await flush();
    await wrapper.find('[data-test="onlyoffice-editor"]').trigger('click');

    await wrapper.find('button').trigger('click');
    await wrapper.find('button').trigger('click');
    await flush();

    expect(mocks.confirm).toHaveBeenCalledTimes(1);
    expect(mocks.closeModal).not.toHaveBeenCalled();
  });

  it('确认框打开后路由跳转会关闭确认框和编辑器', async () => {
    onlineUtils.openOfficeDocument({ file: FILE });
    await flush();
    await wrapper.find('[data-test="onlyoffice-editor"]').trigger('click');
    await wrapper.find('button').trigger('click');
    await flush();

    routeHook({ fullPath: '/b' }, { fullPath: '/a' });
    await flush();

    expect(mocks.confirmDestroy).toHaveBeenCalledTimes(1);
    expect(mocks.closeModal).toHaveBeenCalledTimes(1);
    expect(wrapper.find('[data-test="onlyoffice-editor"]').exists()).toBe(false);
  });

  it('onlyoffice 关闭请求也进入应用未保存确认流程', async () => {
    onlineUtils.openOfficeDocument({ file: FILE });
    await flush();
    const editor = wrapper.getComponent({ name: 'OnlyOfficeEditor' });

    editor.vm.$emit('modified', true);
    editor.vm.$emit('close');
    await flush();

    expect(mocks.confirm).toHaveBeenCalledWith(expect.objectContaining({ title: '放弃未保存的更改？' }));
    expect(mocks.closeModal).not.toHaveBeenCalled();
  });

  it('关闭前业务校验失败时使用确认弹窗，由用户确认后继续关闭', async () => {
    const beforeClose = vi.fn().mockRejectedValue(new Error('标注未完成：缺少检验结果'));

    onlineUtils.openOfficeDocument({ beforeClose, file: FILE });
    await flush();
    await wrapper.find('button').trigger('click');
    await flush();

    expect(beforeClose).toHaveBeenCalledTimes(1);
    expect(mocks.confirm).toHaveBeenCalledWith(
      expect.objectContaining({
        cancelText: '返回标注',
        content: '标注未完成：缺少检验结果',
        iconType: 'warning',
        okText: '仍然关闭',
        title: '标注未完成',
      }),
    );
    expect(mocks.messageError).not.toHaveBeenCalled();
    expect(mocks.requestEditorClose).not.toHaveBeenCalled();

    mocks.shouldEditorClose = false;
    mocks.confirm.mock.calls[0]?.[0].onOk();
    await flush();

    expect(mocks.requestEditorClose).toHaveBeenCalledTimes(1);
    expect(mocks.closeModal).not.toHaveBeenCalled();
    wrapper.getComponent({ name: 'OnlyOfficeEditor' }).vm.$emit('close');
    await flush();

    expect(mocks.closeModal).toHaveBeenCalledTimes(1);
    expect(wrapper.find('[data-test="onlyoffice-editor"]').exists()).toBe(false);
  });

  it('关闭校验确认弹窗选择返回标注时保留编辑器', async () => {
    const beforeClose = vi.fn().mockRejectedValue(new Error('标注未完成：缺少检验结果'));

    onlineUtils.openOfficeDocument({ beforeClose, file: FILE });
    await flush();
    await wrapper.find('button').trigger('click');
    await flush();

    mocks.confirm.mock.calls[0]?.[0].onCancel();
    await flush();

    expect(mocks.requestEditorClose).not.toHaveBeenCalled();
    expect(mocks.closeModal).not.toHaveBeenCalled();
    expect(wrapper.find('[data-test="onlyoffice-editor"]').exists()).toBe(true);
  });

  it('校验失败且有未保存更改时合并提示并由应用直接关闭', async () => {
    const beforeClose = vi.fn().mockRejectedValue(new Error('标注未完成：缺少检验结果'));

    onlineUtils.openOfficeDocument({ beforeClose, file: FILE });
    await flush();
    await wrapper.find('[data-test="onlyoffice-editor"]').trigger('click');
    await wrapper.find('button').trigger('click');
    await flush();

    expect(beforeClose).toHaveBeenCalledTimes(1);
    expect(mocks.confirm).toHaveBeenCalledWith(
      expect.objectContaining({
        content: '标注未完成：缺少检验结果\n关闭后，当前未保存的文档更改也将丢失。',
        okText: '仍然关闭',
        title: '标注未完成',
      }),
    );
    mocks.confirm.mock.calls[0]?.[0].onOk();
    await flush();

    expect(mocks.requestEditorClose).not.toHaveBeenCalled();
    expect(mocks.closeModal).toHaveBeenCalled();
    expect(wrapper.find('[data-test="onlyoffice-editor"]').exists()).toBe(false);
  });

  it('未改动文档时关闭仍校验提示但不阻止关闭', async () => {
    const beforeClose = vi.fn().mockRejectedValue(new Error('标注未完成：缺少检验结论'));

    onlineUtils.openOfficeDocument({ beforeClose, file: FILE });
    await flush();
    await wrapper.find('button').trigger('click');
    await flush();

    expect(beforeClose).toHaveBeenCalledTimes(1);
    expect(mocks.confirm).toHaveBeenCalledWith(expect.objectContaining({ content: '标注未完成：缺少检验结论' }));
    mocks.confirm.mock.calls[0]?.[0].onOk();
    await flush();

    expect(mocks.requestEditorClose).toHaveBeenCalledTimes(1);
    expect(mocks.closeModal).toHaveBeenCalled();
  });

  it('编辑状态回落后关闭不误报未保存，但仍由 onlyoffice 完成关闭', async () => {
    onlineUtils.openOfficeDocument({ file: FILE });
    await flush();
    const editor = wrapper.getComponent({ name: 'OnlyOfficeEditor' });

    editor.vm.$emit('modified', true);
    editor.vm.$emit('modified', false);
    await flush();
    await wrapper.find('button').trigger('click');
    await flush();

    expect(mocks.confirm).not.toHaveBeenCalled();
    expect(mocks.requestEditorClose).toHaveBeenCalledTimes(1);
    expect(mocks.closeModal).toHaveBeenCalled();
  });

  it('关闭前模板校验期间显示全屏 loading', async () => {
    let resolveValidation: () => void = () => {};
    const beforeClose = vi.fn().mockReturnValue(new Promise<void>((resolve) => (resolveValidation = resolve)));

    onlineUtils.openOfficeDocument({ beforeClose, file: FILE });
    await flush();
    await wrapper.find('[data-test="onlyoffice-editor"]').trigger('click');
    await wrapper.find('button').trigger('click');
    await flush();

    const mask = wrapper.find('[data-test="document-saving-mask"]');
    expect(mask.exists()).toBe(true);
    expect(mask.attributes('data-phase')).toBe('validating-close');
    expect(mask.text()).toContain('模板校验中');

    resolveValidation();
    await flush();
  });

  it('编辑器配置加载中关闭也执行必要校验', async () => {
    let resolveConfig: (value: any) => void = () => {};
    const beforeClose = vi.fn().mockRejectedValue(new Error('标注未完成：缺少检验结果'));
    mocks.getOnlyOfficeConfig.mockReturnValue(new Promise((resolve) => (resolveConfig = resolve)));

    onlineUtils.openOfficeDocument({ beforeClose, file: FILE });
    await flush();
    await wrapper.find('button').trigger('click');
    await flush();

    expect(beforeClose).toHaveBeenCalledTimes(1);
    expect(mocks.confirm).toHaveBeenCalledWith(expect.objectContaining({ content: '标注未完成：缺少检验结果' }));
    expect(mocks.closeModal).not.toHaveBeenCalled();

    mocks.confirm.mock.calls[0]?.[0].onCancel();
    resolveConfig(CONFIG_RESPONSE);
    await flush();
  });

  it('前一会话只读时不影响下一编辑会话加载期间的关闭校验', async () => {
    mocks.getOnlyOfficeConfig.mockResolvedValueOnce({
      data: { ...CONFIG_RESPONSE.data, editorConfig: { ...CONFIG_RESPONSE.data.editorConfig, mode: 'view' } },
    });
    onlineUtils.openOfficeDocument({ file: FILE, mode: 'edit' });
    await flush();

    let resolveConfig: (value: any) => void = () => {};
    const beforeClose = vi.fn().mockRejectedValue(new Error('标注未完成：缺少检验结果'));
    mocks.getOnlyOfficeConfig.mockReturnValueOnce(new Promise((resolve) => (resolveConfig = resolve)));
    onlineUtils.openOfficeDocument({ beforeClose, file: FILE, mode: 'edit' });
    await flush();
    await wrapper.find('button').trigger('click');
    await flush();

    expect(beforeClose).toHaveBeenCalledTimes(1);
    expect(mocks.confirm).toHaveBeenCalledWith(expect.objectContaining({ content: '标注未完成：缺少检验结果' }));

    mocks.confirm.mock.calls[0]?.[0].onCancel();
    resolveConfig(CONFIG_RESPONSE);
    await flush();
  });

  it('关闭校验期间路由跳转后不再操作已卸载的编辑器', async () => {
    let resolveValidation: () => void = () => {};
    const beforeClose = vi.fn().mockReturnValue(new Promise<void>((resolve) => (resolveValidation = resolve)));

    onlineUtils.openOfficeDocument({ beforeClose, file: FILE });
    await flush();
    await wrapper.find('button').trigger('click');
    await flush();

    routeHook({ fullPath: '/b' }, { fullPath: '/a' });
    resolveValidation();
    await flush();

    expect(mocks.closeModal).toHaveBeenCalledTimes(1);
    expect(mocks.requestEditorClose).not.toHaveBeenCalled();
    expect(wrapper.find('[data-test="onlyoffice-editor"]').exists()).toBe(false);
  });

  it('关闭校验期间路由跳转后校验失败也不弹出过期确认框', async () => {
    let rejectValidation: (error: Error) => void = () => {};
    const beforeClose = vi.fn().mockReturnValue(new Promise<void>((_, reject) => (rejectValidation = reject)));

    onlineUtils.openOfficeDocument({ beforeClose, file: FILE });
    await flush();
    await wrapper.find('button').trigger('click');
    await flush();

    routeHook({ fullPath: '/b' }, { fullPath: '/a' });
    rejectValidation(new Error('标注未完成：缺少检验结果'));
    await flush();

    expect(mocks.closeModal).toHaveBeenCalledTimes(1);
    expect(mocks.confirm).not.toHaveBeenCalled();
    expect(mocks.requestEditorClose).not.toHaveBeenCalled();
  });

  it('后端确认新版文件写回后提示保存成功', async () => {
    mocks.getOnlyOfficeSaveStatus
      .mockResolvedValueOnce({ data: { lastSaveTime: 1000, version: 1 } })
      .mockResolvedValueOnce({ data: { lastSaveTime: 2000, version: 1 } });

    onlineUtils.openOfficeDocument({ file: FILE });
    await flush();
    await wrapper.find('[data-test="onlyoffice-editor"]').trigger('dblclick');
    await flush();

    expect(mocks.getOnlyOfficeSaveStatus).toHaveBeenCalledTimes(2);
    expect(mocks.messageSuccess).toHaveBeenCalledWith('文档保存成功');
    expect(wrapper.find('[data-test="document-saving-mask"]').exists()).toBe(false);
  });

  it('签批页保存完成后向 onSave 回传另存为的上传文件对象', async () => {
    const savedFile = {
      fileExtension: 'docx',
      fileId: 'saved-copy.docx',
      fileSize: 321,
      name: '检验报告.docx',
      url: '/api/file/Image/annex/saved-copy.docx',
    };
    const onSave = vi.fn();
    mocks.getOnlyOfficeSaveStatus
      .mockResolvedValueOnce({ data: { lastSaveTime: 1000, sessionStatus: 'IDLE', version: 1 } })
      .mockResolvedValueOnce({ data: { file: savedFile, lastSaveTime: 2000, sessionStatus: 'EDITING', version: 1 } })
      .mockResolvedValueOnce({ data: { file: savedFile, lastSaveTime: 2000, sessionStatus: 'IDLE', version: 2 } });

    onlineUtils.openOfficeDocument({ file: FILE, mode: 'qianpiye', onSave });
    await flush();
    const editor = wrapper.find('[data-test="onlyoffice-editor"]');
    await editor.trigger('contextmenu');
    await flush();
    await wrapper.find('button').trigger('click');
    await flush();

    expect(onSave).toHaveBeenCalledWith({ file: savedFile, modified: true });
  });

  it('撤销恢复为未修改状态时不显示保存遮罩', async () => {
    vi.useFakeTimers();
    mocks.getOnlyOfficeSaveStatus.mockResolvedValue({ data: { lastSaveTime: 1000, version: 1 } });

    onlineUtils.openOfficeDocument({ file: FILE });
    await flush();
    const editor = wrapper.find('[data-test="onlyoffice-editor"]');
    await editor.trigger('click');
    await editor.trigger('contextmenu');
    await flush();

    expect(wrapper.find('[data-test="document-saving-mask"]').exists()).toBe(false);
    await vi.advanceTimersByTimeAsync(60_000);
    await flush();
    expect(mocks.messageError).not.toHaveBeenCalled();
    vi.useRealTimers();
  });

  it('status 6 延迟三十秒到达时仍能确认保存成功', async () => {
    vi.useFakeTimers();
    const callbackAt = Date.now() + 30_000;
    mocks.getOnlyOfficeSaveStatus.mockImplementation(() => Promise.resolve({ data: { lastSaveTime: Date.now() >= callbackAt ? 2000 : 1000, version: 1 } }));

    onlineUtils.openOfficeDocument({ file: FILE });
    await flush();
    await wrapper.find('[data-test="onlyoffice-editor"]').trigger('dblclick');
    await flush();
    await vi.advanceTimersByTimeAsync(30_000);
    await flush();

    expect(mocks.messageSuccess).toHaveBeenCalledWith('文档保存成功');
    expect(mocks.messageError).not.toHaveBeenCalled();
    expect(wrapper.find('[data-test="document-saving-mask"]').exists()).toBe(false);
    vi.useRealTimers();
  });

  it('保存状态查询在途时路由跳转会取消后续轮询和提示', async () => {
    let resolveSaveStatus: (value: any) => void = () => {};
    mocks.getOnlyOfficeSaveStatus
      .mockResolvedValueOnce({ data: { lastSaveTime: 1000, version: 1 } })
      .mockReturnValueOnce(new Promise((resolve) => (resolveSaveStatus = resolve)));

    onlineUtils.openOfficeDocument({ file: FILE });
    await flush();
    await wrapper.find('[data-test="onlyoffice-editor"]').trigger('dblclick');
    await flush();

    routeHook({ fullPath: '/b' }, { fullPath: '/a' });
    resolveSaveStatus({ data: { lastSaveTime: 2000, version: 1 } });
    await flush();

    expect(mocks.closeModal).toHaveBeenCalledTimes(1);
    expect(mocks.getOnlyOfficeSaveStatus).toHaveBeenCalledTimes(3);
    expect(mocks.messageSuccess).not.toHaveBeenCalled();
    expect(mocks.messageError).not.toHaveBeenCalled();
  });

  it('手动保存后执行业务校验并以警告提示标注未完成', async () => {
    const onManualSave = vi.fn().mockRejectedValue(new Error('标注未完成：缺少检验结论'));
    mocks.getOnlyOfficeSaveStatus
      .mockResolvedValueOnce({ data: { lastSaveTime: 1000, version: 1 } })
      .mockResolvedValueOnce({ data: { lastSaveTime: 2000, version: 1 } });

    onlineUtils.openOfficeDocument({ file: FILE, onManualSave });
    await flush();
    await wrapper.find('[data-test="onlyoffice-editor"]').trigger('dblclick');
    await flush();

    expect(onManualSave).toHaveBeenCalledTimes(1);
    expect(mocks.messageWarning).toHaveBeenCalledWith('标注未完成：缺少检验结论');
    expect(mocks.messageError).not.toHaveBeenCalled();
    expect(mocks.messageSuccess).toHaveBeenCalledWith('文档保存成功');
    expect(wrapper.find('[data-test="onlyoffice-editor"]').exists()).toBe(true);
  });

  it('业务校验悬挂时不超过保存确认窗口，仍按 status 6 确认结果', async () => {
    vi.useFakeTimers();
    const onManualSave = vi.fn().mockReturnValue(new Promise<void>(() => {}));
    mocks.getOnlyOfficeSaveStatus
      .mockResolvedValueOnce({ data: { lastSaveTime: 1000, version: 1 } })
      .mockResolvedValueOnce({ data: { lastSaveTime: 2000, version: 1 } });

    onlineUtils.openOfficeDocument({ file: FILE, onManualSave });
    await flush();
    await wrapper.find('[data-test="onlyoffice-editor"]').trigger('dblclick');
    await flush();
    await vi.advanceTimersByTimeAsync(60_000);
    await flush();

    expect(mocks.messageWarning).toHaveBeenCalledWith('模板校验超时，请检查');
    expect(mocks.messageSuccess).toHaveBeenCalledWith('文档保存成功');
    expect(wrapper.find('[data-test="document-saving-mask"]').exists()).toBe(false);
    vi.useRealTimers();
  });

  it('status 6 到达前不显示保存遮罩，确认写回后才执行模板校验', async () => {
    let resolveValidation: () => void = () => {};
    let resolveSaveStatus: (value: any) => void = () => {};
    const onManualSave = vi.fn().mockReturnValue(new Promise<void>((resolve) => (resolveValidation = resolve)));
    mocks.getOnlyOfficeSaveStatus
      .mockResolvedValueOnce({ data: { lastSaveTime: 1000, version: 1 } })
      .mockReturnValueOnce(new Promise((resolve) => (resolveSaveStatus = resolve)));

    onlineUtils.openOfficeDocument({ file: FILE, onManualSave });
    await flush();
    await wrapper.find('[data-test="onlyoffice-editor"]').trigger('dblclick');
    await flush();

    expect(wrapper.find('[data-test="document-saving-mask"]').exists()).toBe(false);
    expect(onManualSave).not.toHaveBeenCalled();

    resolveSaveStatus({ data: { lastSaveTime: 2000, version: 1 } });
    await flush();
    const mask = wrapper.find('[data-test="document-saving-mask"]');
    expect(mask.attributes('data-phase')).toBe('validating');
    expect(mask.text()).toContain('模板校验中');

    resolveValidation();
    await flush();
    expect(wrapper.find('[data-test="document-saving-mask"]').exists()).toBe(false);
    expect(mocks.messageSuccess).toHaveBeenCalledWith('文档保存成功');
  });

  it('等待 status 6 时不显示保存遮罩且不阻止关闭', async () => {
    let resolveSaveStatus: (value: any) => void = () => {};
    mocks.getOnlyOfficeSaveStatus
      .mockResolvedValueOnce({ data: { lastSaveTime: 1000, version: 1 } })
      .mockReturnValueOnce(new Promise((resolve) => (resolveSaveStatus = resolve)));

    onlineUtils.openOfficeDocument({ file: FILE });
    await flush();
    await wrapper.find('[data-test="onlyoffice-editor"]').trigger('dblclick');
    await flush();

    expect(wrapper.find('[data-test="document-saving-mask"]').exists()).toBe(false);
    await wrapper.find('button').trigger('click');
    expect(mocks.requestEditorClose).toHaveBeenCalledTimes(1);
    expect(mocks.closeModal).toHaveBeenCalledTimes(1);

    resolveSaveStatus({ data: { lastSaveTime: 2000, version: 1 } });
    await flush();
    expect(mocks.messageSuccess).not.toHaveBeenCalled();
  });

  it('未检测到 status 6 时静默结束观察，不误报保存超时', async () => {
    vi.useFakeTimers();
    mocks.getOnlyOfficeSaveStatus.mockResolvedValue({ data: { lastSaveTime: 1000, version: 1 } });

    onlineUtils.openOfficeDocument({ file: FILE });
    await flush();
    await wrapper.find('[data-test="onlyoffice-editor"]').trigger('click');
    await wrapper.find('[data-test="onlyoffice-editor"]').trigger('dblclick');
    await vi.runAllTimersAsync();
    await flush();

    expect(wrapper.find('[data-test="document-saving-mask"]').exists()).toBe(false);
    expect(mocks.messageError).not.toHaveBeenCalled();
    vi.useRealTimers();
  });

  it('路由跳走时自动关闭——编辑器占着 DS 编辑锁，不能留在新页面上', async () => {
    const onClose = vi.fn();
    onlineUtils.openOfficeDocument({ file: FILE, onClose });
    await flush();
    expect(wrapper.find('[data-test="onlyoffice-editor"]').exists()).toBe(true);

    routeHook({ fullPath: '/b' }, { fullPath: '/a' });
    await flush();

    expect(mocks.closeModal).toHaveBeenCalled();
    expect(onClose).toHaveBeenCalledTimes(1);
    expect(wrapper.find('[data-test="onlyoffice-editor"]').exists()).toBe(false);
  });

  it('导航失败或地址没变时不关——页面根本没动，跟着关是误伤', async () => {
    onlineUtils.openOfficeDocument({ file: FILE });
    await flush();

    routeHook({ fullPath: '/b' }, { fullPath: '/a' }, new Error('aborted'));
    routeHook({ fullPath: '/a' }, { fullPath: '/a' });
    await flush();

    expect(mocks.closeModal).not.toHaveBeenCalled();
    expect(wrapper.find('[data-test="onlyoffice-editor"]').exists()).toBe(true);
  });

  it('组件卸载时摘掉路由钩子', () => {
    wrapper.unmount();
    expect(mocks.removeRouteHook).toHaveBeenCalledTimes(1);
  });

  it('关闭后到达的 config 响应不再打开编辑器（防竞态）', async () => {
    let resolveConfig: (value: any) => void = () => {};
    mocks.getOnlyOfficeConfig.mockReturnValue(new Promise((resolve) => (resolveConfig = resolve)));

    onlineUtils.openOfficeDocument({ file: FILE });
    await flush();
    await wrapper.find('button').trigger('click'); // 请求还在途中就关闭
    resolveConfig(CONFIG_RESPONSE);
    await flush();

    expect(wrapper.find('[data-test="onlyoffice-editor"]').exists()).toBe(false);
  });
});
