/* @vitest-environment happy-dom */
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { defHttp } from '#/api/request';
import { createDmsFilePrintTicket, getDmsFilePageCount, renderDmsPrintPdf } from '#/api/x/dms/print';
import { onlineUtils } from '#/utils/jnpf';
import { printManagedDmsDocument } from '../dmsHiprint';
import { prepareDmsPrintAttempt, type DmsPrintAttempt } from '../dmsPrintAttempt';

const pdf = vi.hoisted(() => ({ pages: 1 }));
vi.mock('#/api/request', () => ({ defHttp: { post: vi.fn() } }));
vi.mock('#/api/x/dms/print', () => ({ createDmsFilePrintTicket: vi.fn(), getDmsFilePageCount: vi.fn(), renderDmsPrintPdf: vi.fn() }));
vi.mock('#/utils/jnpf', () => ({ onlineUtils: { hiprint: vi.fn(), isHiprintClientReady: vi.fn() } }));
vi.mock('pdfjs-dist/build/pdf.worker.min.mjs?url', () => ({ default: 'worker' }));
vi.mock('pdfjs-dist', () => ({
  GlobalWorkerOptions: {},
  getDocument: () => ({
    promise: Promise.resolve({
      numPages: pdf.pages,
      destroy: vi.fn(),
      getPage: async () => ({ cleanup: vi.fn(), getViewport: () => ({ height: 842, width: 595 }), render: () => ({ promise: Promise.resolve() }) }),
    }),
  }),
}));

const config = { printTaskId: 'task', fileVersionId: 'version', copies: 1 };
let saved: DmsPrintAttempt;
let confirmations: number;
let rejectReceipt: boolean;
let rejectBeginResponse: boolean;
let deliveries: any[];
const snapshot = () => structuredClone(saved);

describe('durable DMS print recovery', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    const storage = new Map<string, string>();
    vi.stubGlobal('localStorage', {
      getItem: (k: string) => storage.get(k) ?? null,
      setItem: (k: string, v: string) => storage.set(k, v),
      removeItem: (k: string) => storage.delete(k),
      get length() {
        return storage.size;
      },
    });
    pdf.pages = 1;
    confirmations = 0;
    rejectReceipt = false;
    rejectBeginResponse = false;
    deliveries = [];
    saved = { id: 'attempt', tenantId: 'tenant', userId: 'user', taskId: 'task', fileVersionId: 'version', copies: 1, state: 'OPEN', numbersJson: JSON.stringify([{serialNumber:'unique-1',countSerialNumber:'20260001'}]), batchesJson: '[]' };
    vi.mocked(getDmsFilePageCount).mockResolvedValue({ data: { pageCount: 1 } } as any);
    vi.mocked(createDmsFilePrintTicket).mockResolvedValue({ data: { accessTicket: 'ticket', fileName: 'file.pdf', fileVersionId: 'version' } } as any);
    vi.mocked(renderDmsPrintPdf).mockImplementation(async (request: any) => {
      const pages = request.items[0].printPages.split('-').map(Number);
      pdf.pages = (pages[1] ?? pages[0]) - pages[0] + 1;
      return new Blob();
    });
    vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue({} as any);
    vi.spyOn(HTMLCanvasElement.prototype, 'toDataURL').mockReturnValue('data:image/png;base64,x');
    vi.mocked(onlineUtils.isHiprintClientReady).mockResolvedValue(true);
    vi.mocked(onlineUtils.hiprint).mockImplementation(async (options) => {
      deliveries.push(options);
      options.onPrint?.({ mode: 'hiprint' });
      return { mode: 'hiprint' };
    });
    vi.mocked(defHttp.post).mockImplementation(async ({ url, data }: any) => {
      const batches = JSON.parse(saved.batchesJson);
      const batch = batches.find((item: any) => item.id === data.batchId);
      if (url.endsWith('/fasong-jihua') && !batches.length) saved.batchesJson = JSON.stringify(data.batches);
      if (url.endsWith('/kaishi-pici')) {
        if (batch.state !== 'READY') throw new Error('发送结果未知');
        batch.state = 'SENDING';
        batch.token = data.token;
        saved.batchesJson = JSON.stringify(batches);
        if (rejectBeginResponse) throw new Error('许可响应丢失');
      }
      if (url.endsWith('/queren-pici')) {
        if (rejectReceipt) throw new Error('网络断开');
        expect(batch.token).toBe(data.token);
        batch.state = 'SUBMITTED';
        saved.batchesJson = JSON.stringify(batches);
      }
      if (url.endsWith('/queren-fasong')) {
        confirmations++;
        saved.state = 'CONFIRMED';
      }
      return { code: 200, data: snapshot() } as any;
    });
  });
  afterEach(() => {
    vi.useRealTimers();
    vi.restoreAllMocks();
    vi.unstubAllGlobals();
  });

  it('prints two copies with fixed distinct serials and sends each exactly once', async () => {
    saved.copies = 2;
    saved.numbersJson = JSON.stringify([
      {serialNumber:'unique-1',countSerialNumber:'20260001'},
      {serialNumber:'unique-2',countSerialNumber:'20260002'},
    ]);
    await printManagedDmsDocument('file.pdf', {...config,copies:2,watermarkBusinessTypes:['RECORD', 'REPORT']}, snapshot());
    expect(deliveries).toHaveLength(2);
    expect(deliveries.map(item => item.clientOptions.copies)).toEqual([1,1]);
    const requests = vi.mocked(createDmsFilePrintTicket).mock.calls.map(call => call[0]);
    expect(requests.every(item => item.requireSerialWatermark === true)).toBe(true);
    expect(requests.map(item => item.watermarkBusinessTypes)).toEqual([['RECORD', 'REPORT'], ['RECORD', 'REPORT']]);
    expect(requests.map(item => item.customBarcode)).toEqual([
      [{barValue:'unique-1',barText:'打印流水号: unique-1  计数流水号: 20260001'}],
      [{barValue:'unique-2',barText:'打印流水号: unique-2  计数流水号: 20260002'}],
    ]);
    expect(confirmations).toBe(1);
    await printManagedDmsDocument('file.pdf', config, snapshot());
    expect(deliveries).toHaveLength(2);
  });

  it('applies online formatting separately to each frozen copy', async () => {
    saved.copies = 2;
    saved.numbersJson = JSON.stringify([{ serialNumber: 'unique-1', countSerialNumber: '20260001' }, { serialNumber: 'unique-2', countSerialNumber: '20260002' }]);
    const builder = async ({ serialNumber, countSerialNumber }: any) => [{ barValue: serialNumber, barText: `${countSerialNumber} / ${serialNumber}` }];
    await printManagedDmsDocument('file.pdf', { ...config, buildCustomBarcode: builder }, snapshot());
    expect(vi.mocked(createDmsFilePrintTicket).mock.calls.map(([request]) => request.customBarcode)).toEqual([
      [{ barValue: 'unique-1', barText: '20260001 / unique-1' }], [{ barValue: 'unique-2', barText: '20260002 / unique-2' }],
    ]);
  });
  it('validates every pending copy before any ticket or dispatch and allows retry after interface failure', async () => {
    saved.copies = 2;
    saved.numbersJson = JSON.stringify([{ serialNumber: 'unique-1', countSerialNumber: '20260001' }, { serialNumber: 'unique-2', countSerialNumber: '20260002' }]);
    const builder = vi.fn(async ({ serialNumber }: any) => {
      if (serialNumber === 'unique-2') throw new Error('格式接口不可用');
      return [{ barValue: serialNumber, barText: serialNumber }];
    });
    await expect(printManagedDmsDocument('file.pdf', { ...config, buildCustomBarcode: builder }, snapshot())).rejects.toThrow('格式接口不可用');
    expect(createDmsFilePrintTicket).not.toHaveBeenCalled();
    expect(deliveries).toHaveLength(0);
    builder.mockImplementation(async ({ serialNumber }) => [{ barValue: serialNumber, barText: serialNumber }]);
    await printManagedDmsDocument('file.pdf', { ...config, buildCustomBarcode: builder }, snapshot());
    expect(deliveries).toHaveLength(2);
  });
  it('reads each copy format once across page batches', async () => {
    vi.mocked(getDmsFilePageCount).mockResolvedValue({ data: { pageCount: 101 } } as any);
    const builder = vi.fn(async ({ serialNumber }: any) => [{ barValue: serialNumber, barText: '统一文字' }]);
    await printManagedDmsDocument('file.pdf', { ...config, buildCustomBarcode: builder }, snapshot());
    expect(builder).toHaveBeenCalledTimes(1);
    expect(deliveries).toHaveLength(2);
    expect(vi.mocked(createDmsFilePrintTicket).mock.calls.every(([request]) => request.customBarcode?.[0]?.barText === '统一文字')).toBe(true);
  });
  it('rejects a formatter that replaces the fixed barcode identity before sending', async () => {
    await expect(printManagedDmsDocument('file.pdf', { ...config, buildCustomBarcode: () => [{ barValue: 'wrong', barText: 'text' }] }, snapshot())).rejects.toThrow('当前固定流水号');
    expect(createDmsFilePrintTicket).not.toHaveBeenCalled();
    expect(deliveries).toHaveLength(0);
  });
  it('late success after timeout is persisted and confirmed, and recovery never dispatches it again', async () => {
    vi.useFakeTimers();
    vi.mocked(onlineUtils.hiprint).mockImplementation(async (options) => {
      deliveries.push(options);
      return { mode: 'hiprint' };
    });
    const run = printManagedDmsDocument('file.pdf', config, snapshot());
    const failure = expect(run).rejects.toThrow('结果未知');
    await vi.advanceTimersByTimeAsync(60_001);
    await failure;
    expect(deliveries).toHaveLength(1);
    expect(confirmations).toBe(0);
    await expect(printManagedDmsDocument('file.pdf', config, snapshot())).rejects.toThrow('结果未知');
    deliveries[0].onPrint({ mode: 'hiprint' });
    await vi.advanceTimersByTimeAsync(0);
    expect(confirmations).toBe(1);
    expect(saved.state).toBe('CONFIRMED');
    await printManagedDmsDocument('file.pdf', config, snapshot());
    expect(deliveries).toHaveLength(1);
  });

  it('101 pages: failure of the second batch preserves the first 100 pages and resumes only page 101', async () => {
    vi.mocked(getDmsFilePageCount).mockResolvedValue({ data: { pageCount: 101 } } as any);
    const render = vi.mocked(renderDmsPrintPdf).getMockImplementation()!;
    let failed = false;
    vi.mocked(renderDmsPrintPdf).mockImplementation(async (request: any) => {
      if (request.items[0].printPages === '101' && !failed) {
        failed = true;
        throw new Error('第二批转换失败');
      }
      return render(request);
    });
    await expect(printManagedDmsDocument('file.pdf', config, snapshot())).rejects.toThrow('第二批转换失败');
    expect(deliveries).toHaveLength(1);
    expect(confirmations).toBe(0);
    expect(JSON.parse(saved.batchesJson).map((item: any) => item.state)).toEqual(['SUBMITTED', 'READY']);
    const restored = await prepareDmsPrintAttempt(config, 1);
    expect(restored.copies).toBe(1);
    await printManagedDmsDocument('file.pdf', config, restored);
    expect(deliveries).toHaveLength(2);
    expect(confirmations).toBe(1);
    expect(deliveries.map((item) => item.clientOptions.copies)).toEqual([1, 1]);
    expect(vi.mocked(createDmsFilePrintTicket).mock.calls.map((call) => call[0].printPages)).toEqual(['1-100', '101', '101']);
  });

  it('lost dispatch-permission response blocks any send and is not automatically retried', async () => {
    rejectBeginResponse = true;
    await expect(printManagedDmsDocument('file.pdf', config, snapshot())).rejects.toThrow('许可响应丢失');
    rejectBeginResponse = false;
    await expect(printManagedDmsDocument('file.pdf', config, snapshot())).rejects.toThrow('结果未知');
    expect(deliveries).toHaveLength(0);
  });

  it('success with failed receipt persistence is recovered after refresh from the scoped outbox', async () => {
    rejectReceipt = true;
    await expect(printManagedDmsDocument('file.pdf', config, snapshot())).rejects.toThrow('网络断开');
    expect(deliveries).toHaveLength(1);
    expect(localStorage.length).toBe(1);
    const recovered = snapshot(); // 模拟丢弃原页面运行上下文。
    rejectReceipt = false;
    await printManagedDmsDocument('file.pdf', config, recovered);
    expect(deliveries).toHaveLength(1);
    expect(confirmations).toBeGreaterThan(0);
    expect(localStorage.length).toBe(0);
  });

  it('another account cannot consume the original account local success receipt', async () => {
    rejectReceipt = true;
    await expect(printManagedDmsDocument('file.pdf', config, snapshot())).rejects.toThrow();
    rejectReceipt = false;
    const other = { ...snapshot(), userId: 'other' };
    await expect(printManagedDmsDocument('file.pdf', config, other)).rejects.toThrow('结果未知');
    expect(confirmations).toBe(0);
    expect(deliveries).toHaveLength(1);
  });
});
