/* @vitest-environment happy-dom */

import { flushPromises, mount } from '@vue/test-utils';

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

import RecordView from '../index.vue';
import { ElnRecordOpenParamError, resolveElnRecordOpenParams, toElnRecordOfficeConfigQuery, unwrapElnRecordEditorConfig } from '../recordView';

const apiMocks = vi.hoisted(() => ({
  getOnlyOfficeConfig: vi.fn(),
  getOnlyOfficeSaveStatus: vi.fn(),
}));
const messageMocks = vi.hoisted(() => ({
  confirmOptions: undefined as any,
  createConfirm: vi.fn((options: any) => {
    messageMocks.confirmOptions = options;
    return { destroy: vi.fn() };
  }),
  error: vi.fn(),
}));
const editorMocks = vi.hoisted(() => ({ requestClose: vi.fn() }));
const viewParams = vi.hoisted(() => ({ current: {} as Record<string, unknown> }));

vi.mock('#/api/x/onlyoffice', () => ({
  getOnlyOfficeConfig: apiMocks.getOnlyOfficeConfig,
  getOnlyOfficeSaveStatus: apiMocks.getOnlyOfficeSaveStatus,
}));
vi.mock('@jnpf/hooks', () => ({
  useMessage: () => ({ createConfirm: messageMocks.createConfirm, createMessage: { error: messageMocks.error } }),
}));
vi.mock('#/utils/jnpf', () => ({
  onlineUtils: {
    getViewParam: (name: string) => viewParams.current[name],
  },
}));
vi.mock('#/components/OnlyOfficeEditor/src/Editor.vue', async () => {
  const { defineComponent, h } = await import('vue');
  return {
    default: defineComponent({
      name: 'OnlyOfficeEditor',
      emits: ['close', 'modified'],
      props: { config: { required: true, type: Object } },
      setup(_props, { expose }) {
        expose({ requestClose: editorMocks.requestClose });
        return () => h('div', { 'data-test': 'onlyoffice-editor' });
      },
    }),
  };
});

const DOCUMENT = { fileExtension: 'docx', fileId: 'file-1', fileSize: 12, name: 'ELN-001.docx' };
const EDITOR_CONFIG = {
  data: {
    document: { key: 'record-1-r1', title: 'ELN-001.docx', url: 'https://example.test/ELN-001.docx' },
    documentType: 'word',
    editorConfig: { mode: 'edit' },
    token: 'signed-token',
  },
};

describe('eLN record view', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    messageMocks.confirmOptions = undefined;
    editorMocks.requestClose.mockReset();
    viewParams.current = { document: DOCUMENT, recordId: 'record-1', revisionId: 'revision-1' };
    apiMocks.getOnlyOfficeConfig.mockResolvedValue(EDITOR_CONFIG);
    apiMocks.getOnlyOfficeSaveStatus.mockResolvedValue({ data: { lastSaveTime: 1000, sessionStatus: 'IDLE', version: 2 } });
  });

  it('rejects incomplete custom-view parameters before requesting an editor config', () => {
    expect(() => resolveElnRecordOpenParams(() => undefined)).toThrow(ElnRecordOpenParamError);
  });

  it('shows the parameter error without requesting an editor config', async () => {
    viewParams.current = { recordId: 'record-1', revisionId: 'revision-1' };
    const wrapper = mount(RecordView);
    await flushPromises();

    expect(wrapper.find('[data-test="record-error"]').text()).toContain('缺少 ELN 记录文档');
    expect(apiMocks.getOnlyOfficeConfig).not.toHaveBeenCalled();
  });

  it('requests the fixed record fill context while leaving the final mode to the backend', () => {
    const source: Record<string, unknown> = { document: DOCUMENT, recordId: 'record-1', revisionId: 'revision-1' };
    const params = resolveElnRecordOpenParams((name) => source[name]);

    expect(toElnRecordOfficeConfigQuery(params)).toEqual({
      bizDataId: 'record-1',
      bizModule: 'eln.record',
      bizScene: 'eln.record.fill',
      fileId: 'file-1',
      fileName: 'ELN-001.docx',
      fileType: 'annex',
      mode: 'edit',
    });
  });

  it('keeps an explicitly requested readonly record view readonly', () => {
    const source: Record<string, unknown> = {
      document: DOCUMENT,
      mode: 'view',
      recordId: 'record-1',
      revisionId: 'revision-1',
    };
    const params = resolveElnRecordOpenParams((name) => source[name]);

    expect(params.mode).toBe('view');
    expect(toElnRecordOfficeConfigQuery(params).mode).toBe('view');
  });

  it('shows a loading state, then renders the editor after the backend returns an editable config', async () => {
    let resolveConfig: (value: typeof EDITOR_CONFIG) => void = () => {};
    apiMocks.getOnlyOfficeConfig.mockReturnValue(
      new Promise((resolve) => {
        resolveConfig = resolve;
      }),
    );
    const wrapper = mount(RecordView);

    expect(wrapper.find('[data-test="record-loading"]').exists()).toBe(true);
    expect(apiMocks.getOnlyOfficeConfig).toHaveBeenCalledWith({
      bizDataId: 'record-1',
      bizModule: 'eln.record',
      bizScene: 'eln.record.fill',
      fileId: 'file-1',
      fileName: 'ELN-001.docx',
      fileType: 'annex',
      mode: 'edit',
    });

    resolveConfig(EDITOR_CONFIG);
    await flushPromises();

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

  it('accepts a backend downgrade to readonly mode', () => {
    expect(
      unwrapElnRecordEditorConfig({
        data: {
          document: { key: 'record-1-r1' },
          editorConfig: { mode: 'view' },
          token: 'signed-token',
        },
      }).editorConfig.mode,
    ).toBe('view');
  });

  it('leaves source synchronization to the record fill plugin', async () => {
    const wrapper = mount(RecordView);
    await flushPromises();

    expect(wrapper.find('[data-test="source-sync-button"]').exists()).toBe(false);
    expect(wrapper.find('.eln-record-view__toolbar').exists()).toBe(false);
  });

  it('blocks close for unapplied plugin drafts, then waits for the server session to become idle', async () => {
    const wrapper = mount(RecordView);
    await flushPromises();

    window.dispatchEvent(
      new MessageEvent('message', {
        data: {
          bizDataId: 'record-1',
          documentId: 'file-1',
          pendingCount: 2,
          type: 'jnpf:eln-record-fill-state',
          version: 1,
        },
      }),
    );

    const cancelledClose = (wrapper.vm as any).requestClose();
    await flushPromises();
    expect(messageMocks.createConfirm).toHaveBeenCalledTimes(1);
    expect(messageMocks.confirmOptions.content).toContain('2 个字段尚未应用');
    messageMocks.confirmOptions.onCancel();
    await expect(cancelledClose).resolves.toBe(false);
    expect(apiMocks.getOnlyOfficeSaveStatus).not.toHaveBeenCalled();

    wrapper.getComponent({ name: 'OnlyOfficeEditor' }).vm.$emit('modified', true);
    const confirmedClose = (wrapper.vm as any).requestClose();
    await flushPromises();
    await messageMocks.confirmOptions.onOk();
    await expect(confirmedClose).resolves.toBe('pending');

    expect(editorMocks.requestClose).toHaveBeenCalledTimes(1);
    expect(apiMocks.getOnlyOfficeSaveStatus).not.toHaveBeenCalled();
    wrapper.getComponent({ name: 'OnlyOfficeEditor' }).vm.$emit('close');
    await flushPromises();

    expect(apiMocks.getOnlyOfficeSaveStatus).toHaveBeenCalledWith('file-1', 'annex');
    expect(wrapper.find('[data-test="record-editor"]').exists()).toBe(false);
    expect(wrapper.emitted('closeStarted')).toHaveLength(1);
    expect(wrapper.emitted('closeCancelled')).toBeUndefined();
    expect(wrapper.emitted('closeReady')).toHaveLength(1);
    wrapper.unmount();
  });

  it('closes an unmodified editor without starting a save confirmation flow', async () => {
    const wrapper = mount(RecordView);
    await flushPromises();

    await expect((wrapper.vm as any).requestClose()).resolves.toBe(true);

    expect(editorMocks.requestClose).not.toHaveBeenCalled();
    expect(apiMocks.getOnlyOfficeSaveStatus).not.toHaveBeenCalled();
    wrapper.unmount();
  });

  it('closes directly when an unmodified editor requests close', async () => {
    const wrapper = mount(RecordView);
    await flushPromises();

    wrapper.getComponent({ name: 'OnlyOfficeEditor' }).vm.$emit('close');
    await flushPromises();

    expect(apiMocks.getOnlyOfficeSaveStatus).not.toHaveBeenCalled();
    expect(wrapper.emitted('closeStarted')).toBeUndefined();
    expect(wrapper.emitted('closeReady')).toHaveLength(1);
    wrapper.unmount();
  });

  it('reopens through the backend and stays on the record view when saving fails', async () => {
    apiMocks.getOnlyOfficeSaveStatus.mockResolvedValue({ data: { lastSaveTime: 1000, sessionStatus: 'SAVE_FAILED', version: 1 } });
    const wrapper = mount(RecordView);
    await flushPromises();
    wrapper.getComponent({ name: 'OnlyOfficeEditor' }).vm.$emit('modified', true);

    await expect((wrapper.vm as any).requestClose()).resolves.toBe('pending');
    wrapper.getComponent({ name: 'OnlyOfficeEditor' }).vm.$emit('close');
    await flushPromises();

    expect(apiMocks.getOnlyOfficeConfig).toHaveBeenCalledTimes(2);
    await vi.waitFor(() => expect(wrapper.find('[data-test="record-editor"]').exists()).toBe(true));
    expect(wrapper.emitted('closeStarted')).toHaveLength(1);
    expect(wrapper.emitted('closeCancelled')).toHaveLength(1);
    expect(wrapper.emitted('closeReady')).toBeUndefined();
    expect(messageMocks.error).toHaveBeenCalledWith('电子记录保存失败，请联系管理员处理后重试');
    wrapper.unmount();
  });

  it('shows a clear error instead of mounting the editor when the config request fails', async () => {
    apiMocks.getOnlyOfficeConfig.mockRejectedValue(new Error('无权打开该 ELN 记录'));
    const wrapper = mount(RecordView);
    await flushPromises();

    expect(wrapper.find('[data-test="record-error"]').text()).toContain('无权打开该 ELN 记录');
    expect(wrapper.find('[data-test="record-editor"]').exists()).toBe(false);
  });
});
