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

import type { TemplateAnnotationClient } from '../src/api/template-annotation';
import type { TemplateAnnotationContext, TemplateBinding } from '../src/domain/types';
import type { OnlyOfficeBridge } from '../src/onlyoffice/bridge';
import type { OfficeContentControl } from '../src/onlyoffice/types';

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

import Antd, { message } from 'ant-design-vue';
import { createPinia } from 'pinia';
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest';

import App from '../src/App.vue';
import DocumentFieldList from '../src/components/DocumentFieldList.vue';
import FieldEditor from '../src/components/FieldEditor.vue';

beforeAll(() => {
  window.matchMedia ??= () =>
    ({
      addEventListener() {},
      addListener() {},
      dispatchEvent: () => false,
      matches: false,
      media: '',
      onchange: null,
      removeEventListener() {},
      removeListener() {},
    }) as MediaQueryList;
  globalThis.ResizeObserver ??= class ResizeObserver {
    disconnect() {}
    observe() {}
    unobserve() {}
  };
});

const wrappers: VueWrapper[] = [];
afterEach(() => wrappers.splice(0).forEach((wrapper) => wrapper.unmount()));

const activeBinding: TemplateBinding = {
  controlAlias: '结果',
  controlConfig: { placeholder: '请输入' },
  controlRole: 'PRIMARY',
  controlTag: 'eln.field.result',
  controlType: 'TEXT',
  fieldCode: 'result',
  fieldId: 'field',
  fieldVersionId: 'version',
  id: 'binding',
  primaryBindingId: null,
  status: 'ACTIVE',
  templateId: 'template',
  version: 1,
};

function context(bindings: TemplateBinding[] = []): TemplateAnnotationContext {
  return {
    bindings,
    stepConfiguration: {
      singleStep: true,
      steps: [{ id: 'step-1', name: '实验步骤', order: 1 }],
    },
    templateCode: 'TMP',
    templateId: 'template',
    fields: [
      {
        beizhu: null,
        bizEnabled: 'enabled',
        dangqianZiduanBanbenId: 'version',
        id: 'field',
        mubanBianhao: null,
        ziduanBianma: 'result',
        zuoyongFanwei: 'GLOBAL',
        versions: [
          {
            banbenHao: 1,
            binding: null,
            bizStatus: 'PUBLISHED',
            chuangjianShijian: null,
            danwei: null,
            gengxinShijian: null,
            id: 'version',
            kongjianPeizhi: { placeholder: '请输入' },
            piciMoshi: null,
            shifouBitian: 0,
            shujuLeixing: 'TEXT',
            ziduanDingyiId: 'field',
            ziduanMingcheng: '结果',
            zuofeiYuanyin: null,
          },
        ],
      },
    ],
  };
}

function createFixture(canAnnotate = true, initialContext = context()) {
  let lifecycle: Parameters<OnlyOfficeBridge['bindLifecycle']>[0] | undefined;
  const cleanup = vi.fn();
  const bridge: OnlyOfficeBridge = {
    applyControlPlan: vi.fn().mockResolvedValue(undefined),
    bindLifecycle: vi.fn((callbacks) => {
      lifecycle = callbacks;
      return cleanup;
    }),
    close: vi.fn(),
    deleteControl: vi.fn(),
    disableTrackRevisions: vi.fn().mockResolvedValue(undefined),
    getRuntimeOptions: vi.fn(() => ({
      apiBaseUrl: 'http://localhost:30000',
      documentId: 'file-1',
      bizDataId: 'record-1',
      bizModule: 'eln.template',
      bizScene: 'eln.template.annotate',
      pluginVersion: '0.1.0',
      scope: 'onlyoffice:eln-template-annotate',
      ticket: 'ticket',
      canAnnotate,
    })),
    insertBinding: vi.fn().mockResolvedValue(undefined),
    insertReadOnlyBinding: vi.fn().mockResolvedValue(undefined),
    insertField: vi.fn(),
    insertReference: vi.fn().mockResolvedValue(undefined),
    isFormulaResultLocked: vi.fn().mockResolvedValue(false),
    lockFormulaResult: vi.fn().mockResolvedValue({ primaryTag: 'eln.field.result', locks: [] }),
    readSelectedText: vi.fn().mockResolvedValue(''),
    restoreFormulaResultLocks: vi.fn().mockResolvedValue(undefined),
    unlockFormulaResult: vi.fn().mockResolvedValue(undefined),
    listControls: vi.fn().mockResolvedValue([]),
    selectControl: vi.fn().mockResolvedValue(undefined),
    snapshotControls: vi.fn((bindings: TemplateBinding[], controls: OfficeContentControl[]) =>
      bindings
        .filter((binding) => controls.some((control) => control.Tag === binding.controlTag))
        .map((binding) => ({ bindingId: binding.id, controlTag: binding.controlTag })),
    ),
  };
  const client: TemplateAnnotationClient = {
    activateBinding: vi.fn().mockResolvedValue(undefined),
    cancelBinding: vi.fn().mockResolvedValue(undefined),
    changeCheckboxGroup: vi.fn().mockImplementation(async (_context, _bindingId, form) => ({
      ...activeBinding,
      checkboxGroupCode: form.checkboxGroupCode,
      version: form.expectedVersion + 1,
    })),
    changeCheckboxGroupRule: vi.fn().mockImplementation(async (_context, groupCode, form) => ({
      groupCode,
      maxSelected: form.maxSelected,
      minSelected: form.minSelected,
      version: form.expectedVersion + 1,
    })),
    checkBindingRemoval: vi.fn().mockResolvedValue({ controls: [], dependencies: [], expectedVersions: { binding: 1 }, operationCode: 'remove' }),
    checkBindingVersionUpgrade: vi.fn(),
    checkReferencePromotion: vi.fn(),
    confirmBindingRemoval: vi.fn().mockResolvedValue(undefined),
    confirmBindingVersionUpgrade: vi.fn(),
    confirmReferencePromotion: vi.fn(),
    confirmRepair: vi.fn(),
    confirmVersionCopy: vi.fn(),
    fetchAnnotationContext: vi.fn().mockResolvedValue(initialContext),
    fetchCrossTemplateScalarCandidates: vi.fn().mockResolvedValue([]),
    fetchCrossTemplateSourceTemplates: vi.fn().mockResolvedValue([]),
    fetchRepairContext: vi.fn(),
    reservePrimaryBinding: vi.fn().mockResolvedValue({ ...activeBinding, status: 'PENDING', version: 0 }),
    reserveCrossTemplateScalarBinding: vi.fn(),
    reservePrivateField: vi.fn(),
    reserveReferenceBinding: vi.fn(),
    activateFormulaBinding: vi.fn().mockResolvedValue(undefined),
    cancelFormulaBinding: vi.fn().mockResolvedValue(undefined),
    checkFormulaBindingRemoval: vi.fn(),
    checkFormulaBindingUpgrade: vi.fn(),
    confirmFormulaBindingRemoval: vi.fn(),
    confirmFormulaBindingUpgrade: vi.fn(),
    confirmFormulaVersionCopy: vi.fn(),
    fetchFormulaMappings: vi.fn().mockResolvedValue({ bindings: [], versions: [] }),
    reserveFormulaBinding: vi.fn(),
    suggestCode: vi.fn().mockResolvedValue('jianyan_jieguo'),
  };
  return { bridge, client, cleanup, ready: () => lifecycle?.onReady() };
}

function mountApp(fixture: null | ReturnType<typeof createFixture>) {
  const wrapper = mount(App, {
    global: {
      plugins: [createPinia(), Antd],
      provide: { onlyOfficeBridge: fixture?.bridge ?? null, ...(fixture ? { templateAnnotationClient: fixture.client } : {}) },
    },
  });
  wrappers.push(wrapper);
  return wrapper;
}

async function openDocumentTask(wrapper: VueWrapper): Promise<void> {
  await wrapper
    .findAll('.ant-tabs-tab')
    .find((tab) => tab.text().includes('文档'))!
    .trigger('click');
}

describe('app', () => {
  it('沿用 Ant Design Vue 默认主题和组件尺寸', () => {
    const wrapper = mountApp(createFixture());
    const configProvider = wrapper.getComponent({ name: 'AConfigProvider' });
    const tabs = wrapper.getComponent({ name: 'ATabs' });

    expect(configProvider.props('theme')).toBeUndefined();
    expect(tabs.props('size')).toBeUndefined();
    expect(configProvider.props('locale')).toMatchObject({ locale: 'zh-cn' });
  });

  it('按标注、文档和公式三个顶层任务页签隔离内容', async () => {
    const fixture = createFixture(true, context([activeBinding]));
    vi.mocked(fixture.bridge.listControls).mockResolvedValue([{ Alias: '结果', InternalId: 'control', Tag: activeBinding.controlTag }]);
    const wrapper = mountApp(fixture);
    fixture.ready();
    await vi.waitFor(() => expect(wrapper.get('[data-test="connection-status"]').text()).toBe('已连接'));

    expect(wrapper.get('[data-test="primary-task-tabs"]').text()).toContain('标注');
    expect(wrapper.get('[data-test="document-tab-count"]').text()).toBe('1');
    expect(wrapper.find('[data-test="formula-tab-count"]').exists()).toBe(false);
    expect(wrapper.findComponent(FieldEditor).exists()).toBe(true);
    expect(wrapper.findComponent(DocumentFieldList).exists()).toBe(false);

    await openDocumentTask(wrapper);
    expect(wrapper.findComponent(FieldEditor).exists()).toBe(false);
    expect(wrapper.findComponent(DocumentFieldList).exists()).toBe(true);
    expect(wrapper.findComponent(DocumentFieldList).props('steps')).toEqual([{ id: 'step-1', name: '实验步骤', order: 1 }]);
  });

  it('初始显示版本和加载状态并禁用编辑', () => {
    const wrapper = mountApp(createFixture());
    expect(wrapper.get('[data-test="plugin-version"]').text()).toBe('v0.1.0');
    expect(wrapper.get('[data-test="connection-status"]').text()).toBe('加载中');
    expect(wrapper.get('[data-test="submit-field"]').attributes('disabled')).toBeDefined();
  });

  it('加载过程只使用一条可原位更新的全局 loading message', async () => {
    vi.useFakeTimers();
    const loadingSpy = vi.spyOn(message, 'loading');
    const wrapper = mountApp(createFixture());

    try {
      await vi.advanceTimersByTimeAsync(400);
      expect(loadingSpy).toHaveBeenCalledWith({ content: '正在连接 ONLYOFFICE...', duration: 0, key: 'eln-template-operation' });
      expect(wrapper.find('[data-test="task-progress"]').exists()).toBe(false);
      expect(wrapper.find('[data-test="runtime-message"]').exists()).toBe(false);
    } finally {
      message.destroy('eln-template-operation');
      loadingSpy.mockRestore();
      vi.useRealTimers();
    }
  });

  it('sDK ready 拉取 Ticket context 并装配字段选择器', async () => {
    const fixture = createFixture();
    const wrapper = mountApp(fixture);
    fixture.ready();
    await vi.waitFor(() => expect(wrapper.get('[data-test="connection-status"]').text()).toBe('已连接'));
    expect(fixture.client.fetchAnnotationContext).toHaveBeenCalledOnce();
    expect(wrapper.findComponent(FieldEditor).props('fields')).toHaveLength(1);
  });

  it('单步骤模板隐藏当前步骤选择器', async () => {
    const fixture = createFixture();
    const wrapper = mountApp(fixture);
    fixture.ready();
    await vi.waitFor(() => expect(wrapper.get('[data-test="connection-status"]').text()).toBe('已连接'));

    expect(wrapper.find('[data-test="current-step"]').exists()).toBe(false);
  });

  it('多步骤模板按全局顺序展示可搜索选择器，选择前禁用标注', async () => {
    const multiStepContext = context();
    multiStepContext.stepConfiguration = {
      singleStep: false,
      steps: [
        { id: 'step-3', name: '复核', order: 3 },
        { id: 'step-2', name: '溶解', order: 2 },
        { id: 'step-1', name: '称量', order: 1 },
        { id: 'step-4', name: '归档', order: 4 },
      ],
    };
    const fixture = createFixture(true, multiStepContext);
    const wrapper = mountApp(fixture);
    fixture.ready();
    await vi.waitFor(() => expect(wrapper.get('[data-test="connection-status"]').text()).toBe('已连接'));

    const selector = wrapper.getComponent({ name: 'ASelect' });
    expect(selector.attributes('data-test')).toBe('current-step');
    expect(selector.props('showSearch')).toBe(true);
    expect(selector.props('optionFilterProp')).toBe('label');
    expect(selector.props('options')).toEqual([
      { label: '称量', value: 'step-1' },
      { label: '溶解', value: 'step-2' },
      { label: '复核', value: 'step-3' },
      { label: '归档', value: 'step-4' },
    ]);
    expect(wrapper.findComponent(FieldEditor).props('disabled')).toBe(true);

    selector.vm.$emit('update:value', 'step-2');
    await wrapper.vm.$nextTick();

    expect(wrapper.findComponent(FieldEditor).props('disabled')).toBe(false);
  });

  it('只读上下文禁用写动作但允许刷新', async () => {
    const fixture = createFixture(false);
    const wrapper = mountApp(fixture);
    fixture.ready();
    await vi.waitFor(() => expect(wrapper.get('[data-test="connection-status"]').text()).toBe('已连接'));
    expect(wrapper.get('[data-test="submit-field"]').attributes('disabled')).toBeDefined();
    await openDocumentTask(wrapper);
    expect(wrapper.get('[data-test="refresh-controls"]').attributes('disabled')).toBeUndefined();
    expect(wrapper.findAll('[data-write-action="true"]').every((item) => item.attributes('disabled') !== undefined)).toBe(true);
  });

  it('修复模式隐藏字段编辑器，等待用户选择位置后才允许恢复目标控件', async () => {
    const fixture = createFixture(true, context([activeBinding]));
    vi.mocked(fixture.bridge.getRuntimeOptions).mockReturnValue({
      apiBaseUrl: 'http://localhost:30000',
      documentId: 'file-1',
      bizDataId: 'record-1',
      bizModule: 'eln.template',
      bizScene: 'eln.template.annotate',
      pluginVersion: '0.1.0',
      scope: 'onlyoffice:eln-template-annotate',
      ticket: 'ticket',
      canAnnotate: true,
      repairBindingId: 'binding',
    });
    vi.mocked(fixture.client.fetchRepairContext).mockResolvedValue({
      binding: activeBinding,
      controls: [{ bindingId: 'binding', controlTag: activeBinding.controlTag }],
      dependencies: [],
      expectedVersions: { binding: 1 },
      operationCode: 'repair',
    });
    vi.mocked(fixture.bridge.listControls)
      .mockResolvedValueOnce([])
      .mockResolvedValueOnce([])
      .mockResolvedValueOnce([{ Alias: '结果', InternalId: 'restored', Tag: activeBinding.controlTag }]);
    const wrapper = mountApp(fixture);
    fixture.ready();
    await vi.waitFor(() => expect(wrapper.find('[data-test="repair-binding"]').exists()).toBe(true));

    expect(wrapper.findComponent(FieldEditor).exists()).toBe(false);
    expect(fixture.bridge.insertBinding).not.toHaveBeenCalled();
    await wrapper.get('[data-test="repair-binding"]').trigger('click');
    await vi.waitFor(() => expect(fixture.client.confirmRepair).toHaveBeenCalledOnce());
    expect(fixture.bridge.insertBinding).toHaveBeenCalledWith(expect.objectContaining({ id: 'binding' }), expect.any(AbortSignal));
    await vi.waitFor(() => expect(wrapper.find('[data-test="missing-binding"]').exists()).toBe(false));
    expect(wrapper.get('[data-test="control-item"]').text()).toContain('结果');
    expect(wrapper.find('.document-filter-badge').exists()).toBe(false);
  });

  it('字段列表升级事件连接到版本升级工作流', async () => {
    const upgradeContext = context([activeBinding]);
    upgradeContext.fields[0]!.versions.push({
      ...upgradeContext.fields[0]!.versions[0]!,
      bizStatus: 'PRE_RELEASE',
      id: 'pre-release-v2',
      ziduanMingcheng: '结果 v2',
    });
    const fixture = createFixture(true, upgradeContext);
    vi.mocked(fixture.client.checkBindingVersionUpgrade).mockResolvedValue({
      controls: [],
      dependencies: [],
      expectedVersions: { binding: 1 },
      operationCode: 'upgrade',
    });
    const wrapper = mountApp(fixture);
    fixture.ready();
    await vi.waitFor(() => expect(wrapper.get('[data-test="connection-status"]').text()).toBe('已连接'));
    await openDocumentTask(wrapper);
    expect(wrapper.findComponent(DocumentFieldList).props('fields')).toHaveLength(2);

    wrapper.findComponent(DocumentFieldList).vm.$emit('upgrade-binding', 'binding', 'pre-release-v2');
    await vi.waitFor(() => expect(fixture.client.confirmBindingVersionUpgrade).toHaveBeenCalledOnce());
  });

  it('fieldEditor 透传选择组规则并在标注成功后保留组件实例', async () => {
    const fixture = createFixture();
    vi.mocked(fixture.bridge.listControls)
      .mockResolvedValueOnce([])
      .mockResolvedValueOnce([])
      .mockResolvedValueOnce([{ Alias: '结果', InternalId: 'new', Tag: activeBinding.controlTag }]);
    const wrapper = mountApp(fixture);
    fixture.ready();
    await vi.waitFor(() => expect(wrapper.findComponent(FieldEditor).props('fields')).toHaveLength(1));
    const fieldEditor = wrapper.findComponent(FieldEditor);
    const fieldEditorId = fieldEditor.vm.$.uid;
    const checkboxField = { ...fieldEditor.props('fields')[0], type: 'CHECKBOX' as const };

    fieldEditor.vm.$emit('submit', checkboxField, { groupCode: 'inspection_result', maxSelected: 2, minSelected: 1 });
    await vi.waitFor(() => expect(fixture.client.activateBinding).toHaveBeenCalledOnce());
    expect(fixture.client.reservePrimaryBinding).toHaveBeenCalledWith(
      expect.anything(),
      expect.objectContaining({ checkboxGroupCode: 'inspection_result', checkboxGroupMaxSelected: 2, checkboxGroupMinSelected: 1 }),
    );
    expect(fixture.bridge.insertBinding).toHaveBeenCalledOnce();
    expect(wrapper.findComponent(FieldEditor).vm.$.uid).toBe(fieldEditorId);
    expect(wrapper.findComponent(FieldEditor).props('resetToken')).toBe(1);
    await openDocumentTask(wrapper);
    expect(wrapper.get('[data-test="control-item"]').text()).toContain('结果');
  });

  it('fieldEditor 使用当前 annotator 的编码建议方法', async () => {
    const fixture = createFixture();
    const wrapper = mountApp(fixture);
    fixture.ready();
    await vi.waitFor(() => expect(wrapper.findComponent(FieldEditor).props('fields')).toHaveLength(1));

    const suggestCode = wrapper.findComponent(FieldEditor).props('suggestCode');
    await expect(suggestCode?.('检验结果')).resolves.toBe('jianyan_jieguo');
    expect(fixture.client.suggestCode).toHaveBeenCalledWith(expect.objectContaining({ ticket: 'ticket' }), { text: '检验结果' });
  });

  it('fieldEditor 使用编辑器当前选中文本预填字段名称', async () => {
    const fixture = createFixture();
    vi.mocked(fixture.bridge.readSelectedText).mockResolvedValue('是否合格');
    const wrapper = mountApp(fixture);
    fixture.ready();
    await vi.waitFor(() => expect(wrapper.findComponent(FieldEditor).props('fields')).toHaveLength(1));

    await wrapper.get('[data-test="open-private-field"]').trigger('click');
    await vi.waitFor(() => expect(wrapper.find('[data-test="private-field-name"]').exists()).toBe(true));

    expect(fixture.bridge.readSelectedText).toHaveBeenCalledOnce();
    expect(wrapper.get('[data-test="private-field-name"]').attributes('value')).toBe('是否合格');
  });

  it('文档列表刷新、定位和正式删除事件均连接到工作流', async () => {
    const initialContext = context([activeBinding]);
    initialContext.checkboxGroups = [{ groupCode: 'inspection_result', maxSelected: 2, minSelected: 1, version: 2 }];
    const fixture = createFixture(true, initialContext);
    vi.mocked(fixture.bridge.listControls).mockResolvedValue([{ Alias: '结果', InternalId: 'control', Tag: activeBinding.controlTag }]);
    const wrapper = mountApp(fixture);
    fixture.ready();
    await vi.waitFor(() => expect(wrapper.get('[data-test="connection-status"]').text()).toBe('已连接'));
    await openDocumentTask(wrapper);
    expect(wrapper.find('[data-test="control-item"]').exists()).toBe(true);
    wrapper.findComponent(DocumentFieldList).vm.$emit('select', 'control');
    await vi.waitFor(() => expect(fixture.bridge.selectControl).toHaveBeenCalledWith('control', expect.any(AbortSignal)));
    wrapper.findComponent(DocumentFieldList).vm.$emit(
      'change-checkbox-group',
      'binding',
      { groupCode: 'inspection_result', maxSelected: 2, minSelected: 1 },
      1,
    );
    await vi.waitFor(() =>
      expect(fixture.client.changeCheckboxGroup).toHaveBeenCalledWith(expect.anything(), 'binding', {
        checkboxGroupCode: 'inspection_result',
        checkboxGroupMaxSelected: 2,
        checkboxGroupMinSelected: 1,
        expectedVersion: 1,
        operationCode: expect.any(String),
      }),
    );
    await vi.waitFor(() =>
      expect((wrapper.findComponent(DocumentFieldList).props('bindings') ?? [])[0]!).toMatchObject({
        checkboxGroupCode: 'inspection_result',
        version: 2,
      }),
    );
    wrapper.findComponent(DocumentFieldList).vm.$emit('change-checkbox-group-rule', 'inspection_result', 0, null, 2);
    await vi.waitFor(() =>
      expect(fixture.client.changeCheckboxGroupRule).toHaveBeenCalledWith(expect.anything(), 'inspection_result', {
        expectedVersion: 2,
        maxSelected: null,
        minSelected: 0,
        operationCode: expect.any(String),
      }),
    );
    await vi.waitFor(() =>
      expect(wrapper.findComponent(DocumentFieldList).props('checkboxGroups')).toContainEqual({
        groupCode: 'inspection_result',
        maxSelected: null,
        minSelected: 0,
        version: 3,
      }),
    );
    wrapper.findComponent(DocumentFieldList).vm.$emit('refresh');
    await vi.waitFor(() => expect(fixture.client.fetchAnnotationContext).toHaveBeenCalledTimes(2));
    vi.mocked(fixture.bridge.listControls).mockResolvedValue([]);
    wrapper.findComponent(DocumentFieldList).vm.$emit('remove-binding', 'binding', 'control');
    await vi.waitFor(() => expect(fixture.client.confirmBindingRemoval).toHaveBeenCalledOnce());
  });

  it('初始化错误显示 fatal，事件处理不产生未处理拒绝', async () => {
    const fixture = createFixture();
    vi.mocked(fixture.client.fetchAnnotationContext).mockRejectedValue(new Error('context 失败'));
    const wrapper = mountApp(fixture);
    fixture.ready();
    await vi.waitFor(() => expect(wrapper.get('[data-test="connection-status"]').text()).toBe('连接异常'));
    expect(wrapper.text()).toContain('读取模板标注失败：context 失败');
    expect(wrapper.find('[data-test="task-error"]').exists()).toBe(false);
    await wrapper.get('[data-test="retry-initialization"]').trigger('click');
    await vi.waitFor(() => expect(fixture.client.fetchAnnotationContext).toHaveBeenCalledTimes(2));
  });

  it('卸载时清理 lifecycle', () => {
    const fixture = createFixture();
    const wrapper = mountApp(fixture);
    wrapper.unmount();
    expect(fixture.cleanup).toHaveBeenCalledOnce();
  });

  it('sDK 缺失时显示 fatal 并禁用操作', async () => {
    const wrapper = mountApp(null);
    expect(wrapper.text()).toContain('ONLYOFFICE SDK 未加载，无法初始化插件');
    expect(wrapper.get('[data-test="submit-field"]').attributes('disabled')).toBeDefined();
    await openDocumentTask(wrapper);
    expect(wrapper.get('[data-test="refresh-controls"]').attributes('disabled')).toBeDefined();
  });
});
