/* @vitest-environment happy-dom */

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

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

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

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

const mocks = vi.hoisted(() => ({
  afterEach: vi.fn(),
  closeModal: vi.fn(),
  getOnlyOfficeConfig: vi.fn(),
  getOnlyOfficeSaveStatus: vi.fn(),
  messageError: vi.fn(),
  messageSuccess: vi.fn(),
  openModal: 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: vi.fn(), createMessage: { error: mocks.messageError, success: mocks.messageSuccess } }),
}));

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?.());
      },
    }),
    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,
}));

// 真实编辑器会去加载 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', 'save'],
    setup(_, { emit, expose }) {
      expose({
        requestClose() {
          mocks.requestEditorClose();
          if (mocks.shouldEditorClose) emit('close');
        },
      });
      return () =>
        h('div', {
          'data-test': 'onlyoffice-editor',
          onClick: () => emit('modified', true),
          onDblclick: () => emit('save'),
        });
    },
  }),
}));

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.closeModal.mockClear();
    mocks.removeRouteHook.mockClear();
    mocks.getOnlyOfficeConfig.mockReset();
    mocks.getOnlyOfficeConfig.mockResolvedValue(CONFIG_RESPONSE);
    mocks.getOnlyOfficeSaveStatus.mockReset();
    mocks.getOnlyOfficeSaveStatus.mockResolvedValue({ data: { lastSaveTime: null, version: 1 } });
    mocks.messageSuccess.mockClear();
    mocks.messageError.mockClear();
    mocks.requestEditorClose.mockClear();
    mocks.shouldEditorClose = true;
    mocks.afterEach.mockReset();
    mocks.afterEach.mockImplementation((hook) => {
      routeHook = hook;
      return mocks.removeRouteHook;
    });
    wrapper = mount(GlobalOfficeDocumentModal);
  });

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

  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', mode: 'edit' }),
    );
    expect(wrapper.find('[data-test="onlyoffice-editor"]').exists()).toBe(true);
  });

  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('后端把 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(onSave).toHaveBeenCalledWith({ file: FILE, modified: true });
    expect(onClose).toHaveBeenCalledTimes(1);
    expect(mocks.closeModal).toHaveBeenCalled();
  });

  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('ONLYOFFICE 取消关闭时保留外层弹窗和编辑器', async () => {
    const onClose = vi.fn();
    mocks.shouldEditorClose = false;

    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.requestEditorClose).toHaveBeenCalledTimes(1);
    expect(mocks.closeModal).not.toHaveBeenCalled();
    expect(onClose).not.toHaveBeenCalled();
    expect(wrapper.find('[data-test="onlyoffice-editor"]').exists()).toBe(true);
  });

  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.messageSuccess).toHaveBeenCalledWith('文档保存成功');
    expect(wrapper.find('[data-test="document-saving-mask"]').exists()).toBe(false);
  });

  it('保存期间显示全屏 loading 并阻止关闭', 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"]').text()).toContain('文档保存中');
    await wrapper.find('button').trigger('click');
    expect(mocks.requestEditorClose).not.toHaveBeenCalled();
    expect(mocks.closeModal).not.toHaveBeenCalled();

    resolveSaveStatus({ data: { lastSaveTime: 2000, version: 1 } });
    await flush();
  });

  it('保存确认超时后解除 loading 并提示重试', 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('dblclick');
    await vi.runAllTimersAsync();
    await flush();

    expect(wrapper.find('[data-test="document-saving-mask"]').exists()).toBe(false);
    expect(mocks.messageError).toHaveBeenCalledWith('文档保存确认超时，请重试');
    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);
  });
});
