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

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

import { isInputTableInteractiveTarget } from '../../InputTable/src/helpers/rowSelection';
import UploadFile from './UploadFile.vue';

const mocks = vi.hoisted(() => ({
  preview: vi.fn(),
  nativePreview: vi.fn(),
  documentServer: 'http://office',
  download: vi.fn(),
  legacy: vi.fn(),
  pack: vi.fn(),
  open: vi.fn(),
  image: vi.fn(),
  save: vi.fn(),
  warning: vi.fn(),
  replace: vi.fn(),
  context: {} as any,
}));
vi.mock('@jnpf/hooks', () => ({ useGlobSetting: () => ({ apiURL: '/dev' }) }));
vi.mock('@jnpf/ui', () => ({ createImgPreview: mocks.image }));
vi.mock('@jnpf/utils', () => ({ downloadByUrl: mocks.save, toFileSize: () => '' }));
vi.mock('@vben/stores', () => ({ useUserStore: () => ({ getUserInfo: {} }) }));
vi.mock('ant-design-vue', () => ({
  Form: { useInjectFormItemContext: () => ({ onFieldChange: vi.fn() }) },
  message: { warning: mocks.warning, error: vi.fn() },
}));
vi.mock('#/api/onlineDev/uploadTemplate', () => ({ selectUploadTemplate: vi.fn() }));
vi.mock('#/api/core/common', () => ({ getDownloadUrl: mocks.legacy, getPackDownloadUrl: mocks.pack }));
vi.mock('#/api/onlineDev/workflowFile', () => ({ getWorkflowPreview: mocks.preview, getWorkflowDownload: mocks.download }));
vi.mock('#/components/OnlyOfficeEditor/src/loadDocsApi', () => ({ getDocumentServerUrl: () => mocks.documentServer }));
vi.mock('#/locales', () => ({ $t: (key: string) => key }));
vi.mock('#/utils/jnpf', () => ({ onlineUtils: { openOfficeDocument: mocks.open } }));
vi.mock('./Preview.vue', () => ({ default: { template: '<div />', setup: (_: any, { expose }: any) => expose({ init: mocks.nativePreview }) } }));
vi.mock('./SimpleUploader/FileUploader.vue', () => ({ default: { template: '<div />' } }));
vi.mock('./useUploadOnlineEdit', () => ({
  useUploadOnlineEdit: () => ({
    blocked: ref(false),
    failure: ref(''),
    canDiscard: ref(false),
    canEdit: () => false,
    replace: mocks.replace,
    previewEnabled: ref(true),
    templateFeatureEnabled: ref(false),
    accessContext: () => ({ ...mocks.context }),
    formalFile: (file: any) => file,
  }),
}));
const file = { fileId: 'A', name: 'a.docx', fileExtension: 'docx', fileSize: 1, url: '/raw/A', thumbUrl: '' };
const wrappers: ReturnType<typeof mount>[] = [];
function setup(controlled = true) {
  const wrapper = mount(UploadFile, {
    props: {
      detailed: true,
      value: [file],
      showAllDownload: false,
      ...(controlled ? { fileAccess: { status: 'ready' as const, actions: { VIEW: true, DOWNLOAD: true } } } : {}),
    },
    global: { stubs: { AButton: true }, mocks: { $t: (key: string) => key } },
  });
  wrappers.push(wrapper);
  return wrapper;
}
const viewSelector = '[title="component.upload.view"]';
const downloadSelector = '[title="component.upload.download"]';
beforeEach(() => {
  vi.clearAllMocks();
  mocks.documentServer = 'http://office';
  mocks.context = reactive({ modelId: 'form', recordId: 'record', taskId: 'task', field: 'files' });
  mocks.preview.mockResolvedValue({ data: { kind: 'office', ticket: 'ticket', fileName: 'a.docx', file } });
  mocks.download.mockResolvedValue({ data: { downloadVo: { url: '/native-download' }, downloadName: 'a.docx' } });
  mocks.legacy.mockResolvedValue({ data: { url: '/old-download' } });
});
afterEach(() => wrappers.splice(0).forEach((w) => w.unmount()));
it('uses workflow context for preview even with onlineEdit disabled', async () => {
  await setup().get(viewSelector).trigger('click');
  await flushPromises();
  expect(mocks.preview).toHaveBeenCalledWith(expect.objectContaining({ recordId: 'record', field: 'files', fileId: 'A' }), 'office');
  expect(mocks.open).toHaveBeenCalledWith(expect.objectContaining({ bizScene: 'workflow.uploadPreview', file: expect.objectContaining({ fileId: 'ticket' }) }));
});
it('shows comments only when the workflow explicitly enables review', async () => {
  const wrapper = setup();
  await wrapper.setProps({ fileAccess: { status: 'ready', reviewEnabled: true, actions: { VIEW: true, REVIEW: false } } });
  await wrapper.get(viewSelector).trigger('click');
  await flushPromises();
  expect(mocks.open).toHaveBeenCalledWith(expect.objectContaining({ mode: 'viewComments' }));
  mocks.open.mockClear();
  await wrapper.setProps({ fileAccess: { status: 'ready', actions: { VIEW: true } } });
  await wrapper.get(viewSelector).trigger('click');
  await flushPromises();
  expect(mocks.open).toHaveBeenCalledWith(expect.objectContaining({ mode: 'view' }));
});
it.each(['0f857eea0b07410f99ea35dc816cf5d2', '2312c264f497ddbd86b04bb1805620c3'])(
  'shows audit document comments without granting review for %s',
  async (modelId) => {
    mocks.context = reactive({ modelId, recordId: 'record', taskId: 'task', field: 'wenjian' });
    const wrapper = setup();
    await wrapper.setProps({ fileAccess: { status: 'ready', reviewEnabled: false, actions: { VIEW: true, REVIEW: false, EDIT: false } } });
    await wrapper.get(viewSelector).trigger('click');
    await flushPromises();
    expect(mocks.open).toHaveBeenCalledWith(expect.objectContaining({ mode: 'viewComments', bizScene: 'workflow.uploadPreview' }));
    mocks.open.mockClear();
    mocks.preview.mockResolvedValue({ data: { kind: 'office', ticket: 'ticket', fileName: 'a.xlsx', file } });
    await wrapper.get(viewSelector).trigger('click');
    await flushPromises();
    expect(mocks.open).toHaveBeenCalledWith(expect.objectContaining({ mode: 'view' }));
  },
);
it('keeps legacy preview and download on their existing paths', async () => {
  const wrapper = setup(false);
  await wrapper.get(viewSelector).trigger('click');
  await wrapper.get(downloadSelector).trigger('click');
  await flushPromises();
  expect(mocks.preview).not.toHaveBeenCalled();
  expect(mocks.download).not.toHaveBeenCalled();
  expect(mocks.legacy).toHaveBeenCalledWith('annex', 'A');
  expect(mocks.open).toHaveBeenCalledWith({ file, fileType: 'annex', mode: 'view' });
});
it('does not fall back to a raw link when authorization fails', async () => {
  mocks.preview.mockRejectedValue(new Error('denied'));
  mocks.download.mockRejectedValue(new Error('denied'));
  const wrapper = setup();
  await wrapper.get(viewSelector).trigger('click');
  await wrapper.get(downloadSelector).trigger('click');
  await flushPromises();
  expect(mocks.open).not.toHaveBeenCalled();
  expect(mocks.legacy).not.toHaveBeenCalled();
  expect(mocks.save).not.toHaveBeenCalled();
});
it('drops a preview response after switching records', async () => {
  let done!: (value: any) => void;
  mocks.preview.mockReturnValue(
    new Promise((resolve) => {
      done = resolve;
    }),
  );
  await setup().get(viewSelector).trigger('click');
  mocks.context.recordId = 'other';
  done({ data: { kind: 'office', ticket: 'old', fileName: 'a.docx' } });
  await flushPromises();
  expect(mocks.open).not.toHaveBeenCalled();
});
it('drops a pending download after revocation', async () => {
  let done!: (value: any) => void;
  mocks.download.mockReturnValue(
    new Promise((resolve) => {
      done = resolve;
    }),
  );
  const wrapper = setup();
  await wrapper.get(downloadSelector).trigger('click');
  await wrapper.setProps({ fileAccess: { status: 'ready', actions: { VIEW: true, DOWNLOAD: false } } });
  done({ data: { downloadVo: { url: '/native-download' }, downloadName: 'a.docx' } });
  await flushPromises();
  expect(mocks.save).not.toHaveBeenCalled();
});
it('blocks unsaved child rows instead of omitting their context', async () => {
  mocks.context.parentField = 'rows';
  mocks.context.rowId = '';
  await setup().get(viewSelector).trigger('click');
  expect(mocks.warning).toHaveBeenCalled();
  expect(mocks.preview).not.toHaveBeenCalled();
});
it('uses the original image address after backend authorization', async () => {
  mocks.preview.mockResolvedValue({ data: { kind: 'image', url: '/api/file/Image/annex/A' } });
  await setup().get(viewSelector).trigger('click');
  await flushPromises();
  expect(mocks.image).toHaveBeenCalledWith({ imageList: ['/dev/api/file/Image/annex/A'] });
});

it('uses the native download result without prefixing its URL again', async () => {
  await setup().get(downloadSelector).trigger('click');
  await flushPromises();
  expect(mocks.save).toHaveBeenCalledWith({ fileName: 'a.docx', url: '/native-download' });
  expect(mocks.legacy).not.toHaveBeenCalled();
});
it('delegates platform preview to the existing modal when OnlyOffice is not configured', async () => {
  mocks.documentServer = '';
  mocks.preview.mockResolvedValue({ data: { kind: 'platform', file, url: 'http://preview/native' } });
  await setup().get(viewSelector).trigger('click');
  await flushPromises();
  expect(mocks.preview).toHaveBeenCalledWith(expect.objectContaining({ fileId: 'A' }), 'platform');
  expect(mocks.nativePreview).toHaveBeenCalledWith(file, { url: 'http://preview/native', download: expect.any(Function) });
  expect(mocks.open).not.toHaveBeenCalled();
  mocks.nativePreview.mock.calls[0]![1].download();
  await flushPromises();
  expect(mocks.download).toHaveBeenCalled();
  expect(mocks.legacy).not.toHaveBeenCalled();
});

it('keeps readonly child attachment preview interactive without enabling mutation or selecting the row', async () => {
  mocks.context.parentField = 'rows';
  mocks.context.rowId = 'row-1';
  const wrapper = setup();
  await wrapper.setProps({
    disabled: true,
    detailed: false,
    fileAccess: { status: 'ready', actions: { VIEW: true, DOWNLOAD: false, EDIT: false, ADD: false, REMOVE: false, REPLACE: false } },
  });
  const preview = wrapper.get(viewSelector);
  expect(isInputTableInteractiveTarget(preview.element)).toBe(true);
  expect(wrapper.get('a-button-stub').attributes('disabled')).toBe('true');
  expect(wrapper.findAll(downloadSelector)).toHaveLength(0);
  expect(wrapper.find('[title="在线编辑"]').exists()).toBe(false);
  // 即使人工触发不可用动作，也不能修改附件。
  await wrapper.get('[title="component.upload.del"]').trigger('click');
  expect(mocks.replace).not.toHaveBeenCalled();
  await preview.trigger('click');
  await flushPromises();
  expect(mocks.preview).toHaveBeenCalledWith(expect.objectContaining({ parentField: 'rows', rowId: 'row-1', fileId: 'A' }), 'office');
  expect(mocks.open).toHaveBeenCalledOnce();
  await wrapper.setProps({ fileAccess: { status: 'ready', actions: { VIEW: false } } });
  expect(wrapper.findAll(viewSelector)).toHaveLength(0);
});

it('does not change legacy disabled cell click-through behavior', async () => {
  const wrapper = setup(false);
  await wrapper.setProps({ disabled: true });
  expect(isInputTableInteractiveTarget(wrapper.get(viewSelector).element)).toBe(false);
  expect(wrapper.find('.workflow-file-action').exists()).toBe(false);
});

it('activates single and batch download when node permission is granted and removes both on revocation', async () => {
  mocks.context.parentField = 'rows';
  mocks.context.rowId = 'row-2';
  const wrapper = setup();
  await wrapper.setProps({ disabled: true, showAllDownload: true, fileAccess: { status: 'ready', actions: { VIEW: true, DOWNLOAD: false } } });
  expect(wrapper.findAll(downloadSelector)).toHaveLength(0);
  expect(wrapper.find('.link-text').exists()).toBe(false);
  await wrapper.setProps({ fileAccess: { status: 'ready', actions: { VIEW: true, DOWNLOAD: true } } });
  expect(isInputTableInteractiveTarget(wrapper.get(downloadSelector).element)).toBe(true);
  expect(isInputTableInteractiveTarget(wrapper.get('.link-text').element)).toBe(true);
  await wrapper.get(downloadSelector).trigger('click');
  await flushPromises();
  await wrapper.get('.link-text').trigger('click');
  await flushPromises();
  expect(mocks.download).toHaveBeenCalledTimes(2);
  expect(mocks.download).toHaveBeenLastCalledWith([expect.objectContaining({ parentField: 'rows', rowId: 'row-2', fileId: 'A' })]);
  expect(mocks.save).toHaveBeenCalledTimes(2);
  await wrapper.setProps({ fileAccess: { status: 'ready', actions: { VIEW: true, DOWNLOAD: false } } });
  expect(wrapper.findAll(downloadSelector)).toHaveLength(0);
  expect(wrapper.find('.link-text').exists()).toBe(false);
});
