/* @vitest-environment happy-dom */

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

import { defHttp } from '#/api/request';
import { createDmsFileDownloadTicket, downloadDmsFile } from '#/api/x/dms/personalFolder';
import { onlineUtils } from '#/utils/jnpf';

import { normalizeOfficeDocumentConfig, resolveFileName, resolveOnlyOfficeEditorMode, validateOfficeDocumentConfig } from '../helpers/officeDocument';

const mocks = vi.hoisted(() => ({ imagePreview: vi.fn(), openDmsReprint: vi.fn() }));

vi.mock('#/router', () => ({ router: { push: vi.fn() } }));
vi.mock('#/api/request', () => ({ defHttp: { get: vi.fn(), post: vi.fn() } }));
vi.mock('@jnpf/ui', () => ({ createImgPreview: mocks.imagePreview }));
vi.mock('#/views/x/dms/shared/dmsHiprint', () => ({
  normalizeDmsPrintOptions: (options: any) => ({ copies: options?.copies ?? 1, pages: options?.pages, pageVariables: options?.pageVariables ?? [] }),
}));

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

const FILE = { fileId: 'file_001', name: '检验报告.docx' };

describe('onlineUtils.openOfficeDocument', () => {
  it('emits OPEN_OFFICE_DOCUMENT with config', () => {
    const emitter = onlineUtils.getEmitter();
    const listener = vi.fn();
    emitter.on('OPEN_OFFICE_DOCUMENT', listener);

    const config = { file: FILE, mode: 'view' as const };
    onlineUtils.openOfficeDocument(config);

    expect(listener).toHaveBeenCalledWith(config);
    emitter.off('OPEN_OFFICE_DOCUMENT', listener);
  });

  it('does not emit when fileId is missing', () => {
    const emitter = onlineUtils.getEmitter();
    const listener = vi.fn();
    const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
    emitter.on('OPEN_OFFICE_DOCUMENT', listener);

    onlineUtils.openOfficeDocument({ file: { name: '无 id.docx' } as any });

    expect(listener).not.toHaveBeenCalled();
    expect(errorSpy).toHaveBeenCalledWith('[onlineUtils.openOfficeDocument] file.fileId is required');
    emitter.off('OPEN_OFFICE_DOCUMENT', listener);
    errorSpy.mockRestore();
  });
  it('notifies the caller when no document window is listening', () => {
    const onOpenError = vi.fn();
    onlineUtils.openOfficeDocument({ file: FILE, onOpenError });
    expect(onOpenError).toHaveBeenCalledWith(expect.objectContaining({ message: '文档窗口尚未就绪，请刷新页面后重试' }));
  });
});

describe('onlineUtils.previewDmsFile', () => {
  beforeEach(() => {
    vi.mocked(defHttp.get).mockReset();
    vi.mocked(defHttp.post).mockReset();
    mocks.imagePreview.mockReset();
  });

  it('重打发送能力只透传给预览，不在入口执行', async () => {
    vi.mocked(defHttp.post).mockResolvedValue({ data: { accessTicket: 'ticket', fileVersionId: 'version', fileName: '文件.docx' } });
    const send = vi.fn(); const listener = vi.fn();
    const emitter = onlineUtils.getEmitter(); emitter.on('OPEN_OFFICE_DOCUMENT', listener);
    try {
      await onlineUtils.previewDmsFile({ wenjianZhongxinId: 'version', showPrintButton: true,
        reprint: { requestId: 'request', printRecordId: 'record', send }, printOptions: { copies: 1 },
      });
      expect(listener.mock.calls[0]![0].dmsPrint.reprint.send).toBe(send);
      expect(send).not.toHaveBeenCalled();
    } finally { emitter.off('OPEN_OFFICE_DOCUMENT', listener); }
  });
  it('blocks repeated print-preview entry without a global mask and releases the guard on failure', async () => {
    let rejectTicket!: (error: Error) => void;
    vi.mocked(defHttp.post).mockImplementationOnce(() => new Promise((_resolve, reject) => { rejectTicket = reject; }));
    const options = { wenjianZhongxinId: 'version', showPrintButton: true };
    const opening = onlineUtils.previewDmsFile(options);
    const failed = expect(opening).rejects.toThrow('ticket failed');
    await vi.waitFor(() => expect(defHttp.post).toHaveBeenCalledTimes(1));
    expect(document.querySelector('[data-jnpf-loading]')).toBeNull();
    await expect(onlineUtils.previewDmsFile(options)).resolves.toBe(false);
    expect(defHttp.post).toHaveBeenCalledTimes(1);
    rejectTicket(new Error('ticket failed'));
    await failed;
    expect(document.querySelector('[data-jnpf-loading]')).toBeNull();
    vi.mocked(defHttp.post).mockRejectedValueOnce(new Error('retry reached'));
    await expect(onlineUtils.previewDmsFile(options)).rejects.toThrow('retry reached');
    expect(document.querySelector('[data-jnpf-loading]')).toBeNull();
  });

  it('preserves a global loading mask owned by the caller when preview entry fails', async () => {
    onlineUtils.showLoading('caller');
    const callerMask = document.querySelector('[data-jnpf-loading]');
    try {
      vi.mocked(defHttp.post).mockRejectedValueOnce(new Error('ticket failed'));
      await expect(onlineUtils.previewDmsFile({ wenjianZhongxinId: 'version', showPrintButton: true })).rejects.toThrow('ticket failed');
      expect(document.querySelector('[data-jnpf-loading]')).toBe(callerMask);
    } finally {
      onlineUtils.hideLoading();
    }
  });

  it('opens the preview directly when a file-center version ID is provided', async () => {
    vi.mocked(defHttp.post).mockResolvedValue({
      data: {
        accessTicket: 'direct-preview-ticket',
        fileName: '直接预览.docx',
        fileVersionId: 'direct-file-center-version',
        sizeBytes: 2048,
      },
    } as any);
    const listener = vi.fn();
    const emitter = onlineUtils.getEmitter();
    emitter.on('OPEN_OFFICE_DOCUMENT', listener);

    await expect(onlineUtils.previewDmsFile({ wenjianZhongxinId: ' direct-file-center-version ' })).resolves.toBe(true);

    expect(defHttp.get).not.toHaveBeenCalled();
    expect(defHttp.post).toHaveBeenCalledWith(
      {
        data: expect.objectContaining({
          previewFormat: 'pdf',
          resourceId: 'direct-file-center-version',
          resourceType: 'FILE_VERSION',
          watermarkType: '',
        }),
        url: '/api/dms/file-preview-tickets',
      },
      { errorMessageMode: 'none' },
    );
    expect(listener).toHaveBeenCalledWith({
      dmsAccessTicket: 'direct-preview-ticket',
      dmsPreviewExpiresAt: undefined,
      refreshDmsPreviewTicket: expect.any(Function),
      file: { fileId: 'direct-file-center-version', fileSize: 2048, name: '直接预览.docx' },
      fileType: 'dms',
      mode: 'view',
      title: '直接预览.docx',
    });
    emitter.off('OPEN_OFFICE_DOCUMENT', listener);
  });

  it.each(['download', 'print', 'preview', 'other'])('passes watermarkType %s to initial and refreshed preview tickets', async (watermarkType) => {
    vi.mocked(defHttp.post).mockResolvedValue({
      data: { accessTicket: 'preview-ticket', fileName: 'quality.docx', fileVersionId: 'version-1' },
    } as any);
    const listener = vi.fn();
    const emitter = onlineUtils.getEmitter();
    emitter.on('OPEN_OFFICE_DOCUMENT', listener);

    await onlineUtils.previewDmsFile({ wenjianZhongxinId: 'version-1', watermarkType });
    const config = listener.mock.calls[0]![0];
    await config.refreshDmsPreviewTicket();

    expect(vi.mocked(defHttp.post).mock.calls.map(([request]) => request.data.watermarkType)).toEqual([watermarkType, watermarkType]);
    emitter.off('OPEN_OFFICE_DOCUMENT', listener);
  });

  it('allows callers to retain the source preview format', async () => {
    vi.mocked(defHttp.post).mockResolvedValue({
      data: {
        accessTicket: 'source-preview-ticket',
        fileName: '直接预览.docx',
        fileVersionId: 'direct-file-center-version',
      },
    } as any);

    await onlineUtils.previewDmsFile({
      previewFormat: 'source',
      wenjianZhongxinId: 'direct-file-center-version',
    });

    expect(defHttp.post).toHaveBeenCalledWith(
      {
        data: expect.objectContaining({ previewFormat: 'source' }),
        url: '/api/dms/file-preview-tickets',
      },
      { errorMessageMode: 'none' },
    );
  });

  it('opens image tickets with the JNPF image preview instead of OnlyOffice', async () => {
    vi.mocked(defHttp.post).mockResolvedValue({
      data: {
        accessTicket: 'image-preview-ticket',
        fileName: 'evidence.png',
        fileVersionId: 'image-version',
        mimeType: 'image/png',
      },
    } as any);
    const listener = vi.fn();
    const emitter = onlineUtils.getEmitter();
    emitter.on('OPEN_OFFICE_DOCUMENT', listener);

    await expect(onlineUtils.previewDmsFile({ wenjianZhongxinId: 'image-version' })).resolves.toBe(true);

    expect(mocks.imagePreview).toHaveBeenCalledWith({
      imageList: [expect.stringContaining('/api/file/Dms/OnlyOffice/image-version?ticket=image-preview-ticket')],
    });
    expect(listener).not.toHaveBeenCalled();
    emitter.off('OPEN_OFFICE_DOCUMENT', listener);
  });

  it('rejects conflicting file locators before creating a ticket', async () => {
    await expect(
      onlineUtils.previewDmsFile({
        versionId: 'business-version',
        wenjianZhongxinId: 'file-center-version',
      }),
    ).rejects.toThrow('DMS 文件定位参数只能使用');

    expect(defHttp.get).not.toHaveBeenCalled();
    expect(defHttp.post).not.toHaveBeenCalled();
  });

  it('creates a preview ticket with an explicit DMS resource locator', async () => {
    vi.mocked(defHttp.post).mockResolvedValue({
      data: {
        accessTicket: 'document-preview-ticket',
        fileName: '受控文件.docx',
        fileVersionId: 'fixed-file-version',
      },
    } as any);

    await expect(onlineUtils.previewDmsFile({ resourceId: 'document-1', resourceType: 'DOCUMENT' })).resolves.toBe(true);

    expect(defHttp.post).toHaveBeenCalledWith(
      {
        data: expect.objectContaining({ resourceId: 'document-1', resourceType: 'DOCUMENT' }),
        url: '/api/dms/file-preview-tickets',
      },
      { errorMessageMode: 'none' },
    );
  });

  it('keeps page variables in the normalized front-end print context', async () => {
    vi.mocked(defHttp.post).mockResolvedValue({
      data: {
        accessTicket: 'preview-ticket',
        fileName: '受控文件.docx',
        fileVersionId: 'fixed-file-version',
      },
    } as any);
    const listener = vi.fn();
    const beforePrint = vi.fn();
    const confirmPrint = vi.fn();
    const onPrintSuccess = vi.fn();
    const emitter = onlineUtils.getEmitter();
    emitter.on('OPEN_OFFICE_DOCUMENT', listener);

    await onlineUtils.previewDmsFile({
      beforePrint,
      confirmPrint,
      printQuantity: { total: 10, printed: 2 },
      closeOnPrintSuccess: true,
      onPrintSuccess,
      resourceId: 'file-version-1',
      resourceType: 'FILE_VERSION',
      showPrintButton: true,
      printOptions: { copies: 2, pages: '1-2', pageVariables: [{ pageNo: 1, variables: { code: 'A-001' } }] },
    });

    expect(defHttp.post).toHaveBeenCalledWith(
      expect.objectContaining({ data: expect.not.objectContaining({ pageVariables: expect.anything() }) }),
      expect.anything(),
    );
    expect(listener).toHaveBeenCalledWith(
      expect.objectContaining({
        dmsPrint: {
          beforePrint,
          confirmPrint,
          printQuantity: { total: 10, printed: 2 },
          closeOnPrintSuccess: true,
          copies: 2,
          fileVersionId: 'fixed-file-version',
          onPrintSuccess,
          pages: '1-2',
          pageVariables: [{ pageNo: 1, variables: { code: 'A-001' } }],
        },
      }),
    );
    emitter.off('OPEN_OFFICE_DOCUMENT', listener);
  });

  it('queries the file-center version and opens a read-only DMS preview', async () => {
    vi.mocked(defHttp.get).mockResolvedValue({ data: { wenjianZhongxinBanbenId: 'file-center-version-1' } } as any);
    vi.mocked(defHttp.post).mockResolvedValue({
      data: {
        accessTicket: 'preview-ticket',
        fileName: '质量文件.docx',
        fileVersionId: 'file-center-version-1',
        sizeBytes: 1024,
      },
    } as any);
    const listener = vi.fn();
    const emitter = onlineUtils.getEmitter();
    emitter.on('OPEN_OFFICE_DOCUMENT', listener);

    await expect(onlineUtils.previewDmsFile({ versionId: ' business-version-1 ' })).resolves.toBe(true);

    expect(defHttp.get).toHaveBeenCalledWith(
      { url: '/api/dms/biz/wenjian/banben/file-versions/business-version-1/file-center-version' },
      { errorMessageMode: 'none' },
    );
    expect(defHttp.post).toHaveBeenCalledWith(
      {
        data: expect.objectContaining({ resourceId: 'file-center-version-1', resourceType: 'FILE_VERSION' }),
        url: '/api/dms/file-preview-tickets',
      },
      { errorMessageMode: 'none' },
    );
    expect(listener).toHaveBeenCalledWith({
      dmsAccessTicket: 'preview-ticket',
      dmsPreviewExpiresAt: undefined,
      refreshDmsPreviewTicket: expect.any(Function),
      file: { fileId: 'file-center-version-1', fileSize: 1024, name: '质量文件.docx' },
      fileType: 'dms',
      mode: 'view',
      title: '质量文件.docx',
    });
    emitter.off('OPEN_OFFICE_DOCUMENT', listener);
  });

  it('filters a forged control status while preserving barcode entries for preview, print and download tickets', async () => {
    vi.mocked(defHttp.post).mockResolvedValue({
      data: { accessTicket: 'scene-ticket', fileName: 'quality.docx', fileVersionId: 'version-1' },
    } as any);
    const options = {
      wenjianZhongxinId: 'version-1',
      customBarcode: [
        { page: 4, barValue: 'REC-004', barText: '中文 <&> {/CUN}' },
        { page: 4, barValue: 'SECOND-004', barText: 'Another label' },
        { barValue: 'EVERY-PAGE', barText: 'All pages' },
      ],
      shifouShoukong: 'no',
    } as any;
    await onlineUtils.previewDmsFile(options);
    await onlineUtils.printDmsFile({ ...options, copies: 2, printPages: '2-3' });
    await createDmsFileDownloadTicket('version-1', options);
    expect(defHttp.post).toHaveBeenNthCalledWith(
      1,
      {
        url: '/api/dms/file-preview-tickets',
        data: expect.objectContaining({ customBarcode: options.customBarcode }),
      },
      { errorMessageMode: 'none' },
    );
    expect(defHttp.post).toHaveBeenNthCalledWith(
      3,
      {
        url: '/api/dms/file-download-tickets',
        data: expect.objectContaining({ customBarcode: options.customBarcode }),
      },
      { errorMessageMode: 'none' },
    );
    expect(defHttp.post).toHaveBeenNthCalledWith(
      2,
      {
        url: '/api/dms/file-print-tickets',
        data: expect.objectContaining({ customBarcode: options.customBarcode, copies: 2, printPages: '2-3' }),
      },
      { errorMessageMode: 'none' },
    );
    vi.mocked(defHttp.post)
      .mock.calls.slice(0, 3)
      .forEach(([request]) => {
        expect(request).toBeDefined();
        expect(request!.data).not.toHaveProperty('shifouShoukong');
      });
  });

  it('loads fixed task barcode parameters and retains the print scene on ticket renewal', async () => {
    const buildCustomBarcode = async ({ serialNumber, countSerialNumber }: any) => [{ barValue: serialNumber, barText: `流水 ${serialNumber} / ${countSerialNumber}` }];
    const customBarcode = await buildCustomBarcode({ serialNumber: 'SERIAL-3', countSerialNumber: '20260003' });
    vi.mocked(defHttp.post).mockImplementation(async (request: any) => {
      if (request.url.endsWith('/yulan-canshu')) return { code: 200, data: { serialNumber: 'SERIAL-3', countSerialNumber: '20260003' } } as any;
      return { data: { accessTicket: 'preview-ticket', fileName: 'quality.docx', fileVersionId: 'version-1' } } as any;
    });
    const listener = vi.fn();
    const emitter = onlineUtils.getEmitter();
    emitter.on('OPEN_OFFICE_DOCUMENT', listener);
    try {
      await onlineUtils.previewDmsFile({ wenjianZhongxinId: 'version-1', showPrintButton: true, printTaskId: 'task-1', buildCustomBarcode });
      expect(listener.mock.calls[0]![0].dmsPrint.buildCustomBarcode).toBe(buildCustomBarcode);
      await listener.mock.calls[0]![0].refreshDmsPreviewTicket();
      expect(defHttp.post).toHaveBeenNthCalledWith(1, {
        url: '/api/dms/biz/wenjian/dayin/renwu/yulan-canshu', data: { taskId: 'task-1', fileVersionId: 'version-1' },
      }, expect.anything());
      expect(defHttp.post).toHaveBeenCalledTimes(3);
      for (const [request] of vi.mocked(defHttp.post).mock.calls.slice(1)) {
        expect(request).toMatchObject({ url: '/api/dms/file-preview-tickets', data: { watermarkType: 'print', customBarcode } });
      }
    } finally {
      emitter.off('OPEN_OFFICE_DOCUMENT', listener);
    }
  });

  it('does not open an unmarked preview or send attempt if loading task parameters fails', async () => {
    vi.mocked(defHttp.post).mockResolvedValue({ code: 400, msg: '无当前打印任务的操作权限' } as any);
    await expect(onlineUtils.previewDmsFile({ wenjianZhongxinId: 'version-1', showPrintButton: true, printTaskId: 'task-1' }))
      .rejects.toThrow('无当前打印任务的操作权限');
    expect(defHttp.post).toHaveBeenCalledTimes(1);
  });

  it('rejects empty barcode parameters instead of silently opening an unmarked preview', async () => {
    vi.mocked(defHttp.post).mockResolvedValue({ code: 200, data: { customBarcode: [] } } as any);
    await expect(onlineUtils.previewDmsFile({ wenjianZhongxinId: 'version-1', showPrintButton: true, printTaskId: 'task-1' }))
      .rejects.toThrow('任务固定流水号缺失');
    expect(defHttp.post).toHaveBeenCalledTimes(1);
  });

  it('retains a copy of barcode entries for the preview toolbar print action', async () => {
    const customBarcode = [{ page: 4, barValue: 'REC-004', barText: 'Page four' }];
    vi.mocked(defHttp.post).mockImplementation(async () => {
      customBarcode[0]!.barText = 'Changed while awaiting the preview ticket';
      return { data: { accessTicket: 'preview-ticket', fileName: 'quality.docx', fileVersionId: 'version-1' } } as any;
    });
    const listener = vi.fn();
    const emitter = onlineUtils.getEmitter();
    emitter.on('OPEN_OFFICE_DOCUMENT', listener);
    await onlineUtils.previewDmsFile({ wenjianZhongxinId: 'version-1', showPrintButton: true, customBarcode });
    customBarcode[0]!.barText = 'Changed by caller';
    expect(listener).toHaveBeenCalledWith(
      expect.objectContaining({
        dmsPrint: expect.objectContaining({
          fileVersionId: 'version-1',
          customBarcode: [{ page: 4, barValue: 'REC-004', barText: 'Page four' }],
        }),
      }),
    );
    emitter.off('OPEN_OFFICE_DOCUMENT', listener);
  });

  it('does not create a preview ticket when the business version has no file-center version', async () => {
    vi.mocked(defHttp.get).mockResolvedValue({ data: {} } as any);

    await expect(onlineUtils.previewDmsFile({ versionId: 'business-version-1' })).resolves.toBe(false);

    expect(defHttp.post).not.toHaveBeenCalled();
  });

  it('shows a development notice when no supported preview parameter is provided', async () => {
    const toastSpy = vi.spyOn(onlineUtils, 'toast').mockImplementation(() => {});

    await expect(onlineUtils.previewDmsFile({ fileId: 'file-1' } as any)).resolves.toBe(false);
    await expect(onlineUtils.previewDmsFile({ versionId: '   ' })).resolves.toBe(false);

    expect(toastSpy).toHaveBeenCalledTimes(2);
    expect(toastSpy).toHaveBeenCalledWith('业务开发中', 'info');
    expect(defHttp.get).not.toHaveBeenCalled();
    expect(defHttp.post).not.toHaveBeenCalled();
    toastSpy.mockRestore();
  });
});

describe('DMS download format', () => {
  it('keeps original downloads compatible and allows requesting a PDF blob', async () => {
    vi.mocked(defHttp.get).mockReset();
    const blob = new Blob(['pdf'], { type: 'application/pdf' });
    vi.mocked(defHttp.get).mockResolvedValue(blob);
    await downloadDmsFile('version-1', 'download-ticket');
    expect(defHttp.get).toHaveBeenLastCalledWith(
      { url: '/api/file/Dms/Download/version-1', params: { ticket: 'download-ticket' }, responseType: 'blob' },
      { errorMessageMode: 'none', isTransformResponse: false },
    );
    await expect(downloadDmsFile('version-1', 'download-ticket', 'pdf')).resolves.toBe(blob);
    expect(defHttp.get).toHaveBeenLastCalledWith(
      {
        url: '/api/file/Dms/Download/version-1',
        params: { ticket: 'download-ticket', format: 'pdf' },
        responseType: 'blob',
        timeout: 300_000,
      },
      { errorMessageMode: 'none', isTransformResponse: false },
    );
  });
});

describe('officeDocument helpers', () => {
  it('文件名缺失时从 url 兜底截取（后端靠扩展名定 documentType，丢了就打不开）', () => {
    expect(resolveFileName({ file: { fileId: 'f1', name: '' } } as any)).toBe('');
    expect(resolveFileName({ file: { fileId: 'f1', name: '', url: '/api/file/annex/a.docx?s=k' } } as any)).toBe('a.docx');
    expect(resolveFileName({ file: { fileId: 'f1', name: '', url: '/api/file/annex/noext' } } as any)).toBe('');
  });

  it('校验必填项', () => {
    expect(() => validateOfficeDocumentConfig({ file: { name: 'a.docx' } } as any)).toThrow('file.fileId is required');
    expect(() => validateOfficeDocumentConfig({ file: { fileId: 'f1', name: '' } } as any)).toThrow('file.name is required');
    expect(() => validateOfficeDocumentConfig({ file: FILE })).not.toThrow();
  });

  it('归一化：mode 默认 edit，标题默认取文件名', () => {
    expect(normalizeOfficeDocumentConfig({ file: FILE })).toMatchObject({ mode: 'edit', title: '检验报告.docx' });
    expect(normalizeOfficeDocumentConfig({ file: FILE, mode: 'view', title: ' 自定义 ' })).toMatchObject({
      mode: 'view',
      title: '自定义',
    });
    expect(normalizeOfficeDocumentConfig({ file: FILE, mode: 'qianpiye' })).toMatchObject({
      bizModule: 'dms',
      bizScene: 'dms.qianpiye',
      mode: 'qianpiye',
    });
    expect(
      normalizeOfficeDocumentConfig({
        bizModule: '签批页管理模块标识',
        bizScene: 'custom.scene',
        file: FILE,
        mode: 'qianpiye',
      }),
    ).toMatchObject({ bizModule: 'dms', bizScene: 'dms.qianpiye' });
    expect(resolveOnlyOfficeEditorMode('qianpiye')).toBe('edit');
    expect(() => normalizeOfficeDocumentConfig({ file: FILE, mode: 'readonly' as any })).toThrow('不支持');
    expect(normalizeOfficeDocumentConfig({ file: FILE, mode: 'review' }).mode).toBe('review');
    expect(resolveOnlyOfficeEditorMode('review')).toBe('edit');
    expect(normalizeOfficeDocumentConfig({ file: FILE, mode: 'viewComments' }).mode).toBe('viewComments');
    expect(resolveOnlyOfficeEditorMode('viewComments')).toBe('view');
  });
});

describe('onlineUtils.openDmsReprint loading', () => {
  it.each([false, true])('does not create global loading and releases the guard after failure=%s', async (fails) => {
    let finish!: () => void;
    mocks.openDmsReprint.mockReset();
    mocks.openDmsReprint.mockImplementationOnce(
      () =>
        new Promise<void>((resolve, reject) => {
          finish = () => (fails ? reject(new Error('reprint failed')) : resolve());
        }),
    );
    const opening = onlineUtils.openDmsReprint('record');
    const result = fails ? expect(opening).rejects.toThrow('reprint failed') : expect(opening).resolves.toBeUndefined();
    await vi.waitFor(() => expect(mocks.openDmsReprint).toHaveBeenCalledTimes(1));
    expect(document.querySelector('[data-jnpf-loading]')).toBeNull();
    await onlineUtils.openDmsReprint('record');
    expect(mocks.openDmsReprint).toHaveBeenCalledTimes(1);
    finish();
    await result;
    expect(document.querySelector('[data-jnpf-loading]')).toBeNull();
    mocks.openDmsReprint.mockResolvedValueOnce(undefined);
    await onlineUtils.openDmsReprint('record');
    expect(mocks.openDmsReprint).toHaveBeenCalledTimes(2);
    expect(document.querySelector('[data-jnpf-loading]')).toBeNull();
  });

  it('preserves loading owned by the caller', async () => {
    onlineUtils.showLoading('caller');
    const mask = document.querySelector('[data-jnpf-loading]');
    try {
      mocks.openDmsReprint.mockRejectedValueOnce(new Error('reprint failed'));
      await expect(onlineUtils.openDmsReprint('record')).rejects.toThrow('reprint failed');
      expect(document.querySelector('[data-jnpf-loading]')).toBe(mask);
    } finally {
      onlineUtils.hideLoading();
    }
  });
});
