/* eslint-disable vue/one-component-per-file, vue/require-default-prop -- Test doubles intentionally stay local to this component suite. */
import type { DmsPersonalFolderFile } from '#/api/x/dms/personalFolder';

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

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

import PersonalFolderPage from '../index.vue';
import pageSource from '../index.vue?raw';

const apiMocks = vi.hoisted(() => ({
  createDmsFileDownloadTicket: vi.fn(),
  downloadDmsFile: vi.fn(),
  evaluatePersonalFolderDownloads: vi.fn(),
  getPersonalFolderFiles: vi.fn(),
  getPersonalFolders: vi.fn(),
}));
const messageMocks = vi.hoisted(() => ({ error: vi.fn(), success: vi.fn() }));
const previewMocks = vi.hoisted(() => ({ open: vi.fn() }));
const utilityMocks = vi.hoisted(() => ({ downloadByData: vi.fn() }));
const formState = vi.hoisted(() => ({ config: undefined as any, setFieldsValue: vi.fn() }));
const tableState = vi.hoisted(() => ({ config: undefined as any }));
const treeState = vi.hoisted(() => ({ setSelectedKeys: vi.fn() }));

vi.mock('#/api/x/dms/personalFolder', () => ({
  createDmsFileDownloadTicket: apiMocks.createDmsFileDownloadTicket,
  downloadDmsFile: apiMocks.downloadDmsFile,
  evaluatePersonalFolderDownloads: apiMocks.evaluatePersonalFolderDownloads,
  getPersonalFolderFiles: apiMocks.getPersonalFolderFiles,
  getPersonalFolders: apiMocks.getPersonalFolders,
}));

vi.mock('#/utils/jnpf', () => ({
  getJnpfAppEnCode: () => '',
  onlineUtils: { hideLoading: vi.fn(), showLoading: vi.fn() },
}));

vi.mock('../../shared/dmsFilePreview', () => ({
  dmsPreviewErrorMessage: (error: unknown) => (error instanceof Error ? error.message : '文件预览失败'),
  openDmsFilePreview: previewMocks.open,
}));

vi.mock('@jnpf/hooks', () => ({
  useMessage: () => ({ createMessage: messageMocks }),
}));

vi.mock('@jnpf/utils', () => ({
  downloadByData: utilityMocks.downloadByData,
  formatToDateTime: (value: string, format?: string) => `${format || 'datetime'}:${value}`,
}));

vi.mock('@jnpf/ui', async () => {
  const { defineComponent, h } = await import('vue');
  return {
    BasicLeftTree: defineComponent({
      name: 'BasicLeftTree',
      props: {
        fieldNames: { default: () => ({}), type: Object },
        loading: Boolean,
        showSearch: Boolean,
        showToolbar: Boolean,
        title: String,
        treeData: { default: () => [], type: Array },
      },
      emits: ['reload', 'select'],
      setup(props, { expose, slots }) {
        expose({ setSelectedKeys: treeState.setSelectedKeys });
        return () =>
          h('aside', { 'data-test': 'folder-tree' }, [JSON.stringify(props.treeData), slots.title?.({ key: '__personal_root__', title: '个人文件夹' })]);
      },
    }),
    ScrollContainer: defineComponent({
      name: 'ScrollContainer',
      setup(_, { slots }) {
        return () => h('div', { 'data-test': 'tile-scroll' }, slots.default?.());
      },
    }),
  };
});

vi.mock('@jnpf/ui/modal', async () => {
  const { defineComponent, h } = await import('vue');
  return {
    BasicModal: defineComponent({
      name: 'BasicModal',
      emits: ['register', 'ok'],
      setup(_, { slots }) {
        return () => h('div', slots.default?.());
      },
    }),
    useModal: () => [vi.fn(), { openModal: vi.fn(), closeModal: vi.fn(), setModalProps: vi.fn() }],
  };
});

vi.mock('@jnpf/ui/form', async () => {
  const { defineComponent, h } = await import('vue');
  return {
    BasicForm: defineComponent({
      name: 'BasicForm',
      emits: ['register', 'reset', 'submit'],
      setup() {
        return () => h('form', { 'data-test': 'search-form' });
      },
    }),
    useForm: (config: any) => {
      formState.config = config;
      return [vi.fn(), { setFieldsValue: formState.setFieldsValue }];
    },
  };
});

vi.mock('@jnpf/ui/vxeTable', async () => {
  const { defineComponent, h } = await import('vue');
  return {
    BasicVxeTable: defineComponent({
      name: 'BasicVxeTable',
      inheritAttrs: false,
      props: {
        dataSource: { default: () => [], type: Array },
        emptyText: String,
        loading: Boolean,
        rowConfig: Object,
      },
      emits: ['cell-dblclick', 'register', 'row-click'],
      setup(props, { slots }) {
        return () =>
          h(
            'div',
            { 'data-test': 'personal-file-table' },
            (props.dataSource as DmsPersonalFolderFile[]).flatMap((record) => [
              slots.fileName?.({ record }),
              slots.documentNo?.({ record }),
              slots.versionLabel?.({ record }),
              slots.createdAt?.({ record }),
              slots.validTo?.({ record }),
              slots.sizeBytes?.({ record }),
              slots.action?.({ record }),
            ]),
          );
      },
    }),
    TableAction: defineComponent({
      name: 'TableAction',
      props: { actions: { default: () => [], type: Array }, stopButtonPropagation: Boolean },
      setup(props) {
        return () => h('div', { 'data-actions': (props.actions as any[]).map((action) => action.label).join(','), 'data-test': 'table-action' });
      },
    }),
    useVxeTable: (config: any) => {
      tableState.config = config;
      return [vi.fn(), {}];
    },
  };
});

vi.mock('ant-design-vue', async () => {
  const { defineComponent, h } = await import('vue');
  return {
    Radio: defineComponent({
      name: 'AntRadio',
      setup(_, { slots }) {
        return () => h('span', slots.default?.());
      },
    }),
    RadioGroup: defineComponent({
      name: 'RadioGroup',
      props: { value: String },
      emits: ['update:value'],
      setup(_, { slots }) {
        return () => h('div', slots.default?.());
      },
    }),
    Breadcrumb: defineComponent({
      name: 'AntBreadcrumb',
      setup(_, { slots }) {
        return () => h('nav', slots.default?.());
      },
    }),
    BreadcrumbItem: defineComponent({
      name: 'BreadcrumbItem',
      setup(_, { slots }) {
        return () => h('span', slots.default?.());
      },
    }),
  };
});

const ButtonStub = defineComponent({
  name: 'AButton',
  inheritAttrs: false,
  props: { loading: Boolean, type: String },
  emits: ['click'],
  setup(_, { attrs, emit, slots }) {
    return () => h('button', { ...attrs, onClick: () => emit('click'), type: 'button' }, slots.default?.());
  },
});

const SlotStub = defineComponent({
  setup(_, { slots }) {
    return () => h('div', slots.default?.());
  },
});

function file(values: Partial<DmsPersonalFolderFile> = {}): DmsPersonalFolderFile {
  return {
    createdAt: '2026-09-01T00:00:00Z',
    documentNo: 'DOC-001',
    fileName: '质量标准.pdf',
    id: 'file-1',
    name: '质量标准',
    resourceType: 'FILE_VERSION',
    sizeBytes: 1536,
    sourceId: 'source-1',
    sourceType: 'DMS',
    versionLabel: 'R01',
    ...values,
  };
}

function mountPage() {
  return mount(PersonalFolderPage, {
    global: {
      directives: { loading: () => {} },
      stubs: {
        'a-button': ButtonStub,
        'a-button-group': SlotStub,
        'a-tooltip': SlotStub,
        FileViewModeSwitch: defineComponent({
          name: 'FileViewModeSwitch',
          props: { value: String },
          emits: ['update:value'],
          setup() {
            return () => h('div');
          },
        }),
        'jnpf-empty': defineComponent({ name: 'JnpfEmpty', props: { description: String }, template: '<div data-test="empty">{{ description }}</div>' }),
      },
    },
  });
}

describe('dms personal folder page', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    formState.config = undefined;
    tableState.config = undefined;
    formState.setFieldsValue.mockResolvedValue(undefined);
    apiMocks.getPersonalFolders.mockResolvedValue({
      data: [
        { code: 'BORROW', name: '借阅' },
        { code: 'DOWNLOAD', name: '下载' },
        { code: 'FAVORITE', name: '收藏' },
      ],
    });
    apiMocks.getPersonalFolderFiles.mockImplementation(async (folderType: string) => ({
      data: [file({ fileName: `${folderType}.pdf`, id: `${folderType}-file` })],
    }));
    apiMocks.evaluatePersonalFolderDownloads.mockImplementation(async (ids: string[]) => ({
      data: ids.map((resourceId) => ({ allowed: true, resourceId })),
    }));
    apiMocks.createDmsFileDownloadTicket.mockResolvedValue({
      data: { accessTicket: 'ticket-1', fileName: 'DOWNLOAD.pdf', fileVersionId: 'DOWNLOAD-file', mimeType: 'application/pdf' },
    });
    apiMocks.downloadDmsFile.mockResolvedValue(new Blob(['pdf'], { type: 'application/pdf' }));
    previewMocks.open.mockResolvedValue(true);
  });

  it('uses the standard JNPF page, tree, form and table without fake pagination', async () => {
    const wrapper = mountPage();

    await vi.waitFor(() => expect(apiMocks.getPersonalFolderFiles).toHaveBeenCalledWith('BORROW', { keyword: undefined, pageSize: 100 }));

    expect(pageSource).toContain('class="jnpf-content-wrapper personal-folder-page"');
    expect(pageSource).toContain('<BasicLeftTree');
    expect(pageSource).toContain('<BasicForm');
    expect(pageSource).toContain('<BasicVxeTable');
    expect(pageSource).toContain('<TableAction');
    expect(pageSource).not.toContain('<ATree');
    expect(pageSource).not.toContain('class="detail-list"');
    expect(tableState.config).toMatchObject({ immediate: false, pagination: false, rowKey: 'id', showIndexColumn: false });
    expect(tableState.config.columns.map((column: any) => column.title)).toEqual(['标题', '文件编号', '版本号', '上传日期', '有效期至', '文件大小']);

    const tree = wrapper.findComponent({ name: 'BasicLeftTree' });
    expect(tree.props()).toMatchObject({
      fieldNames: { children: 'children', key: 'key', title: 'title' },
      showSearch: false,
      showToolbar: true,
      title: '个人文件夹',
    });
    expect(treeState.setSelectedKeys).toHaveBeenCalledWith(['BORROW']);
  });

  it('submits trimmed search text and clears it once before changing folders', async () => {
    const wrapper = mountPage();
    await vi.waitFor(() => expect(apiMocks.getPersonalFolderFiles).toHaveBeenCalledOnce());
    apiMocks.getPersonalFolderFiles.mockClear();

    wrapper.findComponent({ name: 'BasicForm' }).vm.$emit('submit', { keyword: '  标准  ' });
    await vi.waitFor(() => expect(apiMocks.getPersonalFolderFiles).toHaveBeenCalledWith('BORROW', { keyword: '标准', pageSize: 100 }));

    apiMocks.getPersonalFolderFiles.mockClear();
    wrapper.findComponent({ name: 'BasicLeftTree' }).vm.$emit('select', 'DOWNLOAD');
    await vi.waitFor(() => expect(apiMocks.getPersonalFolderFiles).toHaveBeenCalledWith('DOWNLOAD', { keyword: undefined, pageSize: 100 }));
    expect(apiMocks.getPersonalFolderFiles).toHaveBeenCalledOnce();
    expect(formState.setFieldsValue).toHaveBeenCalledWith({ keyword: '' });

    await vi.waitFor(() => {
      const labels = (wrapper.findComponent({ name: 'TableAction' }).props('actions') as any[]).map((action) => action.label);
      expect(labels).toEqual(['预览', '下载']);
    });

    apiMocks.getPersonalFolderFiles.mockClear();
    wrapper.findComponent({ name: 'BasicForm' }).vm.$emit('reset');
    await vi.waitFor(() => expect(apiMocks.getPersonalFolderFiles).toHaveBeenCalledWith('DOWNLOAD', { keyword: undefined, pageSize: 100 }));
    expect(apiMocks.getPersonalFolderFiles).toHaveBeenCalledOnce();
  });

  it('selects rows and previews non-action cells from table events', async () => {
    const wrapper = mountPage();
    await vi.waitFor(() => expect(wrapper.find('.file-name-trigger').exists()).toBe(true));

    const table = wrapper.findComponent({ name: 'BasicVxeTable' });
    const record = file({ fileName: 'BORROW.pdf', id: 'BORROW-file' });
    table.vm.$emit('row-click', { row: record });
    await nextTick();
    expect(wrapper.find('.file-name-trigger').attributes('aria-selected')).toBe('true');

    table.vm.$emit('cell-dblclick', { column: { field: 'fileName' }, row: record });
    await vi.waitFor(() =>
      expect(previewMocks.open).toHaveBeenCalledWith(
        expect.objectContaining({ fileName: 'BORROW.pdf', resourceId: 'BORROW-file', resourceType: 'FILE_VERSION' }),
      ),
    );

    previewMocks.open.mockClear();
    table.vm.$emit('cell-dblclick', { column: { field: 'action' }, row: record });
    await nextTick();
    expect(previewMocks.open).not.toHaveBeenCalled();
  });

  it('keeps Enter preview available in details and tile views', async () => {
    const wrapper = mountPage();
    await vi.waitFor(() => expect(wrapper.find('.file-name-trigger').exists()).toBe(true));

    await wrapper.find('.file-name-trigger').trigger('keydown.enter');
    expect(previewMocks.open).toHaveBeenCalledWith(
      expect.objectContaining({ fileName: 'BORROW.pdf', resourceId: 'BORROW-file', resourceType: 'FILE_VERSION' }),
    );

    previewMocks.open.mockClear();
    wrapper.findComponent({ name: 'FileViewModeSwitch' }).vm.$emit('update:value', 'tiles');
    await nextTick();
    await wrapper.find('.file-tile').trigger('keydown.enter');
    expect(previewMocks.open).toHaveBeenCalledWith(
      expect.objectContaining({ fileName: 'BORROW.pdf', resourceId: 'BORROW-file', resourceType: 'FILE_VERSION' }),
    );
  });

  it('disables expired preview actions', async () => {
    apiMocks.getPersonalFolderFiles.mockResolvedValueOnce({ data: [file({ validTo: '2000-01-01T00:00:00Z' })] });
    const wrapper = mountPage();
    await vi.waitFor(() => expect(wrapper.findComponent({ name: 'TableAction' }).exists()).toBe(true));

    const [previewAction] = wrapper.findComponent({ name: 'TableAction' }).props('actions') as any[];
    expect(previewAction.disabled).toBe(true);
    await previewAction.onClick();
    expect(previewMocks.open).not.toHaveBeenCalled();
  });

  it('downloads through a ticket only from the download folder action', async () => {
    const wrapper = mountPage();
    await vi.waitFor(() => expect(apiMocks.getPersonalFolderFiles).toHaveBeenCalledOnce());

    wrapper.findComponent({ name: 'BasicLeftTree' }).vm.$emit('select', 'DOWNLOAD');
    await vi.waitFor(() => {
      const actions = wrapper.findComponent({ name: 'TableAction' }).props('actions') as any[];
      expect(actions.map((action) => action.label)).toEqual(['预览', '下载']);
    });

    const downloadAction = (wrapper.findComponent({ name: 'TableAction' }).props('actions') as any[])[1];
    await downloadAction.onClick();
    expect(apiMocks.createDmsFileDownloadTicket).not.toHaveBeenCalled();
    wrapper.findComponent({ name: 'BasicModal' }).vm.$emit('ok');
    await vi.waitFor(() => expect(utilityMocks.downloadByData).toHaveBeenCalled());

    expect(apiMocks.createDmsFileDownloadTicket).toHaveBeenCalledWith('DOWNLOAD-file', { watermarkBusinessTypes: ['gerenWenjianjia'] });
    expect(apiMocks.evaluatePersonalFolderDownloads).toHaveBeenCalledWith(['DOWNLOAD-file']);
    expect(apiMocks.downloadDmsFile).toHaveBeenCalledWith('DOWNLOAD-file', 'ticket-1', 'pdf');
    expect(utilityMocks.downloadByData).toHaveBeenCalledWith(expect.any(Blob), 'DOWNLOAD.pdf', 'application/pdf');
  });
  it('downloads Word only after confirming the selected format', async () => {
    apiMocks.getPersonalFolderFiles.mockResolvedValue({ data: [file({ fileName: 'manual.docx', id: 'word-file' })] });
    apiMocks.createDmsFileDownloadTicket.mockResolvedValue({ data: { accessTicket: 'word-ticket', fileName: 'manual.docx', fileVersionId: 'word-file' } });
    const wrapper = mountPage();
    await vi.waitFor(() => expect(apiMocks.getPersonalFolderFiles).toHaveBeenCalledOnce());
    wrapper.findComponent({ name: 'BasicLeftTree' }).vm.$emit('select', 'DOWNLOAD');
    await vi.waitFor(() => expect(wrapper.findComponent({ name: 'TableAction' }).props('actions')).toHaveLength(2));
    const action = (wrapper.findComponent({ name: 'TableAction' }).props('actions') as any[])[1];
    await action.onClick();
    expect(apiMocks.createDmsFileDownloadTicket).not.toHaveBeenCalled();
    wrapper.findComponent({ name: 'BasicModal' }).vm.$emit('ok');
    await vi.waitFor(() => expect(apiMocks.downloadDmsFile).toHaveBeenCalledWith('word-file', 'word-ticket', undefined));
  });
  it('explains expired download authorization without requesting a ticket', async () => {
    apiMocks.getPersonalFolderFiles.mockResolvedValue({ data: [file({ validTo: '2000-01-01T00:00:00Z' })] });
    const wrapper = mountPage();
    await vi.waitFor(() => expect(apiMocks.getPersonalFolderFiles).toHaveBeenCalledOnce());
    wrapper.findComponent({ name: 'BasicLeftTree' }).vm.$emit('select', 'DOWNLOAD');
    await vi.waitFor(() => expect(wrapper.findComponent({ name: 'TableAction' }).props('actions')).toHaveLength(2));
    const action = (wrapper.findComponent({ name: 'TableAction' }).props('actions') as any[])[1];
    expect(action.disabled).toBe(true);
    expect(apiMocks.createDmsFileDownloadTicket).not.toHaveBeenCalled();
  });

  it('disables downloads for expired file lifecycle and exhausted workflow authorization', async () => {
    apiMocks.getPersonalFolderFiles.mockImplementation(async (folderType: string) => ({
      data: folderType === 'DOWNLOAD' ? [file({ id: 'expired', lifecycleStatus: 'EXPIRED' }), file({ id: 'used' })] : [file()],
    }));
    apiMocks.evaluatePersonalFolderDownloads.mockResolvedValue({
      data: [
        { allowed: true, resourceId: 'expired' },
        { allowed: false, resourceId: 'used' },
      ],
    });
    const wrapper = mountPage();
    await vi.waitFor(() => expect(apiMocks.getPersonalFolderFiles).toHaveBeenCalledOnce());
    wrapper.findComponent({ name: 'BasicLeftTree' }).vm.$emit('select', 'DOWNLOAD');
    await vi.waitFor(() => expect(wrapper.findAllComponents({ name: 'TableAction' })).toHaveLength(2));
    expect(wrapper.findAllComponents({ name: 'TableAction' }).map((item) => (item.props('actions') as any[])[1].disabled)).toEqual([true, true]);
    expect(apiMocks.evaluatePersonalFolderDownloads).toHaveBeenCalledWith(['expired', 'used']);
  });

  it('disables download when authorization expires while the page stays open', async () => {
    const deadline = Date.now() + 1000;
    apiMocks.getPersonalFolderFiles.mockResolvedValue({ data: [file({ validTo: new Date(deadline).toISOString() })] });
    const wrapper = mountPage();
    await vi.waitFor(() => expect(apiMocks.getPersonalFolderFiles).toHaveBeenCalledOnce());
    wrapper.findComponent({ name: 'BasicLeftTree' }).vm.$emit('select', 'DOWNLOAD');
    await vi.waitFor(() => expect((wrapper.findComponent({ name: 'TableAction' }).props('actions') as any[])[1].disabled).toBe(false));
    await vi.waitFor(() => expect((wrapper.findComponent({ name: 'TableAction' }).props('actions') as any[])[1].disabled).toBe(true), { timeout: 2000 });
    wrapper.unmount();
  });

  it('disables download as soon as the ticket is issued', async () => {
    apiMocks.downloadDmsFile.mockReturnValue(new Promise(() => {}));
    const wrapper = mountPage();
    await vi.waitFor(() => expect(apiMocks.getPersonalFolderFiles).toHaveBeenCalledOnce());
    wrapper.findComponent({ name: 'BasicLeftTree' }).vm.$emit('select', 'DOWNLOAD');
    await vi.waitFor(() => expect((wrapper.findComponent({ name: 'TableAction' }).props('actions') as any[])[1].disabled).toBe(false));
    (wrapper.findComponent({ name: 'TableAction' }).props('actions') as any[])[1].onClick();
    wrapper.findComponent({ name: 'BasicModal' }).vm.$emit('ok');
    await vi.waitFor(() => expect(apiMocks.downloadDmsFile).toHaveBeenCalledOnce());
    expect((wrapper.findComponent({ name: 'TableAction' }).props('actions') as any[])[1].disabled).toBe(true);
  });

  it('explains expiry while the format dialog is open', async () => {
    const deadline = Date.now() + 60_000;
    apiMocks.getPersonalFolderFiles.mockResolvedValue({ data: [file({ validTo: new Date(deadline).toISOString() })] });
    const wrapper = mountPage();
    await vi.waitFor(() => expect(apiMocks.getPersonalFolderFiles).toHaveBeenCalledOnce());
    wrapper.findComponent({ name: 'BasicLeftTree' }).vm.$emit('select', 'DOWNLOAD');
    await vi.waitFor(() => expect(wrapper.findComponent({ name: 'TableAction' }).props('actions')).toHaveLength(2));
    await (wrapper.findComponent({ name: 'TableAction' }).props('actions') as any[])[1].onClick();
    const clock = vi.spyOn(Date, 'now').mockReturnValue(deadline);
    try {
      wrapper.findComponent({ name: 'BasicModal' }).vm.$emit('ok');
      await nextTick();
      expect(messageMocks.error).toHaveBeenCalledWith('下载授权已过期，请重新提交下载申请');
      expect(apiMocks.createDmsFileDownloadTicket).not.toHaveBeenCalled();
    } finally {
      clock.mockRestore();
    }
  });
});
