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

import type { TemplateAnnotationClient } from '../../src/api/template-annotation';
import { TemplateAnnotationApiError } from '../../src/api/template-annotation';
import { useTemplateAnnotator } from '../../src/composables/useTemplateAnnotator';
import type {
  CrossTemplateScalarCandidate,
  CrossTemplateSourceTemplate,
  TemplateAnnotationContext,
  TemplateBinding,
  TemplateChangeCheck,
  TemplateFieldOption,
  TemplateRepairContext,
} from '../../src/domain/types';
import type { TemplateFormulaBinding } from '../../src/domain/formula-mapping';
import type { OnlyOfficeBridge } from '../../src/onlyoffice/bridge';
import type { OfficeContentControl } from '../../src/onlyoffice/types';

function binding(overrides: Partial<TemplateBinding> = {}): TemplateBinding {
  return {
    controlAlias: '结果',
    controlConfig: { placeholder: '请输入' },
    controlRole: 'PRIMARY',
    controlTag: 'eln.field.result',
    controlType: 'TEXT',
    fieldCode: 'result',
    fieldId: 'field',
    fieldVersionId: 'version',
    id: 'binding',
    primaryBindingId: null,
    status: 'PENDING',
    templateId: 'template',
    version: 0,
    ...overrides,
  };
}

const option: TemplateFieldOption = {
  alias: '结果',
  controlConfig: { placeholder: '请输入' },
  fieldCode: 'result',
  fieldId: 'field',
  fieldVersionId: 'version',
  isCurrent: true,
  label: '结果',
  required: false,
  scope: 'GLOBAL',
  source: 'NONE',
  type: 'TEXT',
  versionStatus: 'PUBLISHED',
};
const crossTemplateCandidate: CrossTemplateScalarCandidate = {
  sourceFieldCode: 'result',
  sourceFieldId: 'source-field',
  sourceFieldName: '来源结果',
  sourceFieldType: 'TEXT',
  sourceFieldVersionId: 'source-version',
  sourceTemplateCode: 'SOURCE-TPL',
  sourceTemplateId: 'source-template',
};
const validOptions = {
  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,
};

function annotationContext(overrides: Partial<TemplateAnnotationContext> = {}): TemplateAnnotationContext {
  return {
    bindings: [],
    fields: [],
    stepConfiguration: {
      singleStep: true,
      steps: [{ id: 'step-1', name: '实验步骤', order: 1 }],
    },
    templateCode: 'TMP',
    templateId: 'template',
    ...overrides,
  } as TemplateAnnotationContext;
}

function check(overrides: Partial<TemplateChangeCheck> = {}): TemplateChangeCheck {
  return { controls: [], dependencies: [], expectedVersions: { binding: 2 }, operationCode: 'check-op', ...overrides };
}

function formulaBinding(overrides: Partial<TemplateFormulaBinding> = {}): TemplateFormulaBinding {
  return {
    formulaVersionId: 'formula-version',
    id: 'formula-binding',
    jieguoSuoKuaizhao: [{ bindingId: 'result-binding', controlRole: 'PRIMARY', controlTag: 'eln.field.result', lock: 'unlocked' }],
    operationCode: 'formula-reserve',
    parameterBindings: { amount: 'input-binding' },
    resultBindingId: 'result-binding',
    status: 'PENDING',
    version: 1,
    ...overrides,
  };
}

function createFixture() {
  let ready: (() => void) | undefined;
  const order: string[] = [];
  const bridge: OnlyOfficeBridge = {
    applyControlPlan: vi.fn(async () => {
      order.push('document');
    }),
    bindLifecycle: vi.fn((callbacks) => {
      ready = callbacks.onReady;
      return vi.fn();
    }),
    close: vi.fn(),
    deleteControl: vi.fn(),
    disableTrackRevisions: vi.fn(async () => {
      order.push('review');
    }),
    getRuntimeOptions: vi.fn(() => validOptions),
    insertBinding: vi.fn(async () => {
      order.push('insert');
    }),
    insertReadOnlyBinding: vi.fn(async () => {
      order.push('insert-cross-template');
    }),
    insertField: vi.fn(),
    insertReference: vi.fn(async () => {
      order.push('insert-reference');
    }),
    isFormulaResultLocked: vi.fn(async () => false),
    listControls: vi.fn(async () => {
      order.push('list');
      return [];
    }),
    lockFormulaResult: vi.fn(async () => ({ primaryTag: 'eln.field.result', locks: [{ tag: 'eln.field.result', lock: 'unlocked' }] })),
    readSelectedText: vi.fn(async () => ''),
    restoreFormulaResultLocks: vi.fn(async () => undefined),
    unlockFormulaResult: vi.fn(async () => undefined),
    selectControl: vi.fn(),
    snapshotControls: vi.fn((bindings: TemplateBinding[], controls: OfficeContentControl[]) => {
      const bindingIds = new Set(bindings.map((item) => item.id));
      return controls.flatMap((control) =>
        control.BindingId && control.Tag && bindingIds.has(control.BindingId) ? [{ bindingId: control.BindingId, controlTag: control.Tag }] : [],
      );
    }),
  };
  const client: TemplateAnnotationClient = {
    activateBinding: vi.fn(async () => {
      order.push('activate');
    }),
    cancelBinding: vi.fn(async () => {
      order.push('cancel');
    }),
    changeCheckboxGroup: vi.fn(async (_context, _bindingId, form) =>
      binding({ checkboxGroupCode: form.checkboxGroupCode, status: 'ACTIVE', version: form.expectedVersion + 1 }),
    ),
    changeCheckboxGroupRule: vi.fn(async (_context, groupCode, form) => ({
      groupCode,
      maxSelected: form.maxSelected,
      minSelected: form.minSelected,
      version: form.expectedVersion + 1,
    })),
    checkBindingRemoval: vi.fn(async () => check()),
    checkBindingVersionUpgrade: vi.fn(async () => check()),
    checkReferencePromotion: vi.fn(async () => check()),
    confirmBindingRemoval: vi.fn(async () => {
      order.push('confirm-remove');
    }),
    confirmBindingVersionUpgrade: vi.fn(async () => {
      order.push('confirm-upgrade');
    }),
    confirmReferencePromotion: vi.fn(async () => {
      order.push('confirm-promote');
    }),
    confirmRepair: vi.fn(async () => {
      order.push('confirm-repair');
    }),
    confirmVersionCopy: vi.fn(async () => {
      order.push('confirm-copy');
    }),
    fetchAnnotationContext: vi.fn(async () => {
      order.push('context');
      return annotationContext();
    }),
    fetchCrossTemplateScalarCandidates: vi.fn(async () => []),
    fetchCrossTemplateSourceTemplates: vi.fn(async () => []),
    fetchRepairContext: vi.fn(async () => ({ ...check(), binding: binding({ status: 'ACTIVE', version: 2 }) })),
    reservePrimaryBinding: vi.fn(async () => {
      order.push('reserve');
      return binding();
    }),
    reserveCrossTemplateScalarBinding: vi.fn(async () => {
      order.push('reserve-cross-template');
      return binding({ id: 'cross-binding', readOnly: true, valueSource: 'CROSS_TEMPLATE' });
    }),
    reservePrivateField: vi.fn(async () => {
      order.push('reserve-private');
      return binding({ fieldId: 'private' });
    }),
    reserveReferenceBinding: vi.fn(async () => {
      order.push('reserve-reference');
      return binding({ controlRole: 'REFERENCE', controlTag: 'eln.ref.result.abc', id: 'reference', primaryBindingId: 'binding' });
    }),
    activateFormulaBinding: vi.fn(async () => {
      order.push('activate-formula');
    }),
    cancelFormulaBinding: vi.fn(async () => {
      order.push('cancel-formula');
    }),
    checkFormulaBindingRemoval: vi.fn(async () => ({
      downstreamFormulaBindingIds: [],
      expectedVersions: { 'formula-binding': 1 },
      operationCode: 'remove-formula',
    })),
    checkFormulaBindingUpgrade: vi.fn(async () => ({
      downstreamFormulaBindingIds: [],
      expectedVersions: { 'formula-binding': 1 },
      operationCode: 'upgrade-formula',
    })),
    confirmFormulaBindingRemoval: vi.fn(async () => {
      order.push('confirm-remove-formula');
    }),
    confirmFormulaBindingUpgrade: vi.fn(async () => {
      order.push('confirm-upgrade-formula');
    }),
    confirmFormulaVersionCopy: vi.fn(async () => {
      order.push('confirm-copy-formula');
    }),
    fetchFormulaMappings: vi.fn(async () => ({ bindings: [], versions: [] })),
    reserveFormulaBinding: vi.fn(async () => {
      order.push('reserve-formula');
      return formulaBinding();
    }),
    suggestCode: vi.fn(async () => 'jianyan_jieguo'),
  };
  return { bridge, client, order, ready: () => ready?.() };
}

describe('useTemplateAnnotator binding workflow', () => {
  beforeEach(() => setActivePinia(createPinia()));

  it('使用当前插件 Ticket 上下文请求编码建议', async () => {
    const fixture = createFixture();
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));

    await expect(api.suggestCode('检验结果')).resolves.toBe('jianyan_jieguo');

    expect(fixture.client.suggestCode).toHaveBeenCalledWith(
      expect.objectContaining({ apiBaseUrl: validOptions.apiBaseUrl, ticket: validOptions.ticket }),
      { text: '检验结果' },
    );
  });

  it('公式 reserve 后锁定结果控件并确认 activate，保存请求不含表达式或计算结果', async () => {
    const fixture = createFixture();
    const result = binding({ id: 'result-binding', status: 'ACTIVE' });
    vi.mocked(fixture.client.fetchAnnotationContext).mockResolvedValue(annotationContext({ bindings: [result] }));
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));

    await api.reserveFormulaBinding({ formulaVersionId: 'formula-version', parameterBindings: { amount: 'input-binding' }, resultBindingId: result.id });

    expect(fixture.client.reserveFormulaBinding).toHaveBeenCalledWith(expect.anything(), {
      formulaVersionId: 'formula-version',
      parameterBindings: { amount: 'input-binding' },
      resultBindingId: result.id,
      operationCode: expect.any(String),
      jieguoSuoKuaizhao: [{ bindingId: result.id, controlRole: 'PRIMARY', controlTag: result.controlTag, lock: 'unlocked' }],
    });
    expect(fixture.bridge.lockFormulaResult).toHaveBeenCalledWith('eln.field.result');
    expect(fixture.client.activateFormulaBinding).toHaveBeenCalledWith(expect.anything(), 'formula-binding', {
      controls: [{ bindingId: result.id, controlRole: 'PRIMARY', controlTag: result.controlTag, lock: 'sdtContentLocked' }],
      expectedVersion: 1,
      operationCode: 'formula-reserve',
    });
  });

  it('公式 activate 失败时先恢复锁再 cancel PENDING', async () => {
    const fixture = createFixture();
    const result = binding({ id: 'result-binding', status: 'ACTIVE' });
    vi.mocked(fixture.client.fetchAnnotationContext).mockResolvedValue(annotationContext({ bindings: [result] }));
    vi.mocked(fixture.client.activateFormulaBinding).mockRejectedValue(new Error('activate failed'));
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));

    await expect(api.reserveFormulaBinding({ formulaVersionId: 'formula-version', parameterBindings: {}, resultBindingId: result.id })).rejects.toThrow(
      'activate failed',
    );

    expect(fixture.bridge.restoreFormulaResultLocks).toHaveBeenCalledBefore(fixture.client.cancelFormulaBinding as ReturnType<typeof vi.fn>);
    expect(fixture.client.cancelFormulaBinding).toHaveBeenCalledOnce();
  });

  it('公式 activate 返回过期 Ticket 后不恢复控件或发送 cancel', async () => {
    const fixture = createFixture();
    const result = binding({ id: 'result-binding', status: 'ACTIVE' });
    vi.mocked(fixture.client.fetchAnnotationContext).mockResolvedValue(annotationContext({ bindings: [result] }));
    vi.mocked(fixture.client.activateFormulaBinding).mockRejectedValue(new TemplateAnnotationApiError(401, 'Ticket 已过期'));
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));

    await expect(api.reserveFormulaBinding({ formulaVersionId: 'formula-version', parameterBindings: {}, resultBindingId: result.id })).rejects.toThrow(
      'Ticket 已过期',
    );

    expect(fixture.bridge.restoreFormulaResultLocks).not.toHaveBeenCalled();
    expect(fixture.client.cancelFormulaBinding).not.toHaveBeenCalled();
  });

  it('启动时对已完整锁定的公式 PENDING 使用原操作码 activate，不重复 reserve', async () => {
    const fixture = createFixture();
    const result = binding({ id: 'result-binding', status: 'ACTIVE' });
    const pending = formulaBinding();
    vi.mocked(fixture.client.fetchAnnotationContext).mockResolvedValue(annotationContext({ bindings: [result] }));
    vi.mocked(fixture.client.fetchFormulaMappings).mockResolvedValue({ bindings: [pending], versions: [] });
    vi.mocked(fixture.bridge.isFormulaResultLocked).mockResolvedValue(true);
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));

    expect(fixture.client.activateFormulaBinding).toHaveBeenCalledWith(
      expect.anything(),
      pending.id,
      expect.objectContaining({ operationCode: pending.operationCode }),
    );
    expect(fixture.client.reserveFormulaBinding).not.toHaveBeenCalled();
    expect(api.formulaBindings[0]?.status).toBe('ACTIVE');
  });

  it('启动时对锁不完整的公式 PENDING 先恢复服务端快照再 cancel', async () => {
    const fixture = createFixture();
    const result = binding({ id: 'result-binding', status: 'ACTIVE' });
    const pending = formulaBinding();
    vi.mocked(fixture.client.fetchAnnotationContext).mockResolvedValue(annotationContext({ bindings: [result] }));
    vi.mocked(fixture.client.fetchFormulaMappings).mockResolvedValue({ bindings: [pending], versions: [] });
    vi.mocked(fixture.bridge.isFormulaResultLocked).mockResolvedValue(false);
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));

    expect(fixture.bridge.restoreFormulaResultLocks).toHaveBeenCalledWith({
      primaryTag: result.controlTag,
      locks: [{ tag: result.controlTag, lock: 'unlocked' }],
    });
    expect(fixture.bridge.restoreFormulaResultLocks).toHaveBeenCalledBefore(fixture.client.cancelFormulaBinding as ReturnType<typeof vi.fn>);
    expect(fixture.client.cancelFormulaBinding).toHaveBeenCalledWith(expect.anything(), pending.id, {
      expectedVersion: pending.version,
      operationCode: pending.operationCode,
      reason: 'PENDING_LOCK_INCOMPLETE',
    });
    expect(api.formulaBindings[0]?.status).toBe('CANCELLED');
  });

  it('启动时服务端快照恢复失败则保留 PENDING 且不 cancel', async () => {
    const fixture = createFixture();
    const result = binding({ id: 'result-binding', status: 'ACTIVE' });
    const pending = formulaBinding();
    vi.mocked(fixture.client.fetchAnnotationContext).mockResolvedValue(annotationContext({ bindings: [result] }));
    vi.mocked(fixture.client.fetchFormulaMappings).mockResolvedValue({ bindings: [pending], versions: [] });
    vi.mocked(fixture.bridge.isFormulaResultLocked).mockResolvedValue(false);
    vi.mocked(fixture.bridge.restoreFormulaResultLocks).mockRejectedValue(new Error('恢复锁失败'));
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));

    expect(fixture.client.cancelFormulaBinding).not.toHaveBeenCalled();
    expect(api.formulaBindings[0]?.status).toBe('PENDING');
    expect(api.store.operation).toMatchObject({ kind: 'error', message: expect.stringContaining('需对账') });
  });

  it('启动时缺少服务端快照的锁不完整 PENDING 保持待对账', async () => {
    const fixture = createFixture();
    const result = binding({ id: 'result-binding', status: 'ACTIVE' });
    const pending = formulaBinding({ jieguoSuoKuaizhao: [] });
    vi.mocked(fixture.client.fetchAnnotationContext).mockResolvedValue(annotationContext({ bindings: [result] }));
    vi.mocked(fixture.client.fetchFormulaMappings).mockResolvedValue({ bindings: [pending], versions: [] });
    vi.mocked(fixture.bridge.isFormulaResultLocked).mockResolvedValue(false);
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));

    expect(fixture.bridge.restoreFormulaResultLocks).not.toHaveBeenCalled();
    expect(fixture.client.cancelFormulaBinding).not.toHaveBeenCalled();
    expect(api.formulaBindings[0]?.status).toBe('PENDING');
    expect(api.store.operation).toMatchObject({ kind: 'error', message: expect.stringContaining('需对账') });
  });

  it('公式 PENDING activate 的 Ticket 过期后停止后续写入', async () => {
    const fixture = createFixture();
    const result = binding({ id: 'result-binding', status: 'ACTIVE' });
    const pending = formulaBinding();
    vi.mocked(fixture.client.fetchAnnotationContext).mockResolvedValue(annotationContext({ bindings: [result] }));
    vi.mocked(fixture.client.fetchFormulaMappings).mockResolvedValue({ bindings: [pending], versions: [] });
    vi.mocked(fixture.bridge.isFormulaResultLocked).mockResolvedValue(true);
    vi.mocked(fixture.client.activateFormulaBinding).mockRejectedValue(new TemplateAnnotationApiError(401, 'Ticket 已过期'));
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('error'));

    expect(fixture.client.cancelFormulaBinding).not.toHaveBeenCalled();
    expect(api.contextStore.context).toBeNull();
  });

  it('启动时只对跨模板标识且已完整锁定的 PENDING 绑定 activate，不重复 reserve', async () => {
    const fixture = createFixture();
    const pending = binding({ id: 'cross-pending', readOnly: true, status: 'PENDING', valueSource: 'CROSS_TEMPLATE', version: 3 });
    vi.mocked(fixture.client.fetchAnnotationContext).mockResolvedValue(annotationContext({ bindings: [pending] }));
    vi.mocked(fixture.bridge.listControls).mockResolvedValue([{ BindingId: pending.id, Lock: 'sdtContentLocked', Tag: pending.controlTag }]);
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));

    expect(fixture.client.activateBinding).toHaveBeenCalledWith(expect.anything(), pending.id, { operationCode: expect.any(String) });
    expect(fixture.client.reserveCrossTemplateScalarBinding).not.toHaveBeenCalled();
    expect(api.bindings[0]).toMatchObject({ id: pending.id, status: 'ACTIVE' });
  });

  it('启动时跨模板 PENDING 锁不完整则先删除控件再 cancel', async () => {
    const fixture = createFixture();
    const pending = binding({ id: 'cross-pending', readOnly: true, status: 'PENDING', valueSource: 'CROSS_TEMPLATE', version: 3 });
    vi.mocked(fixture.client.fetchAnnotationContext).mockResolvedValue(annotationContext({ bindings: [pending] }));
    vi.mocked(fixture.bridge.listControls).mockResolvedValue([{ InternalId: 'cross-control', Lock: 'unlocked', Tag: pending.controlTag }]);
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));

    expect(fixture.bridge.deleteControl).toHaveBeenCalledWith('cross-control');
    expect(fixture.bridge.deleteControl).toHaveBeenCalledBefore(fixture.client.cancelBinding as ReturnType<typeof vi.fn>);
    expect(fixture.client.cancelBinding).toHaveBeenCalledWith(expect.anything(), pending.id, {
      operationCode: expect.any(String),
      reason: 'PENDING_LOCK_INCOMPLETE',
    });
    expect(api.bindings[0]).toMatchObject({ id: pending.id, status: 'CANCELLED' });
  });

  it('公式复制 PENDING 仅在全部结果锁已确认后批量 confirm', async () => {
    const fixture = createFixture();
    const result = binding({ id: 'result-binding', status: 'ACTIVE' });
    const copied = formulaBinding({ id: 'copied-formula', operationCode: 'copy-op' });
    vi.mocked(fixture.client.fetchAnnotationContext).mockResolvedValue(annotationContext({ bindings: [result] }));
    vi.mocked(fixture.client.fetchFormulaMappings).mockResolvedValue({
      bindings: [copied],
      copiedPendingBindingIds: [copied.id],
      upgradeOperationCode: 'copy-op',
      versions: [],
    });
    vi.mocked(fixture.bridge.isFormulaResultLocked).mockResolvedValue(true);
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));

    expect(fixture.client.confirmFormulaVersionCopy).toHaveBeenCalledWith(expect.anything(), {
      controls: [{ bindingId: result.id, controlRole: 'PRIMARY', controlTag: result.controlTag, lock: 'sdtContentLocked' }],
      expectedVersions: { [copied.id]: copied.version },
      upgradeOperationCode: 'copy-op',
    });
    expect(fixture.client.reserveFormulaBinding).not.toHaveBeenCalled();
    expect(api.formulaBindings[0]?.status).toBe('ACTIVE');
  });

  it('升级锁定新结果后锁旧结果失败时立即恢复新结果', async () => {
    const fixture = createFixture();
    const oldResult = binding({ id: 'old-result', status: 'ACTIVE', controlTag: 'eln.field.old' });
    const newResult = binding({ id: 'new-result', status: 'ACTIVE', controlTag: 'eln.field.new' });
    const current = formulaBinding({ id: 'formula-binding', resultBindingId: oldResult.id, status: 'ACTIVE' });
    vi.mocked(fixture.client.fetchAnnotationContext).mockResolvedValue(annotationContext({ bindings: [oldResult, newResult] }));
    vi.mocked(fixture.client.fetchFormulaMappings).mockResolvedValue({ bindings: [current], versions: [] });
    vi.mocked(fixture.client.checkFormulaBindingUpgrade).mockResolvedValue({
      downstreamFormulaBindingIds: [],
      expectedVersions: { [current.id]: current.version },
      operationCode: 'upgrade-op',
    });
    const newSnapshot = { primaryTag: newResult.controlTag, locks: [{ tag: newResult.controlTag, lock: 'unlocked' }] };
    vi.mocked(fixture.bridge.lockFormulaResult).mockResolvedValueOnce(newSnapshot).mockRejectedValueOnce(new Error('旧结果锁失败'));
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));

    await expect(
      api.upgradeFormulaBinding(current.id, { formulaVersionId: 'formula-version-2', parameterBindings: {}, resultBindingId: newResult.id }),
    ).rejects.toThrow('旧结果锁失败');

    expect(fixture.bridge.restoreFormulaResultLocks).toHaveBeenCalledWith(newSnapshot);
    expect(fixture.client.confirmFormulaBindingUpgrade).not.toHaveBeenCalled();
  });

  it('公式升级预检失败后恢复可操作状态并展示完整原因', async () => {
    const fixture = createFixture();
    const result = binding({ id: 'result-binding', status: 'ACTIVE' });
    const current = formulaBinding({ id: 'formula-binding', resultBindingId: result.id, status: 'ACTIVE' });
    vi.mocked(fixture.client.fetchAnnotationContext).mockResolvedValue(annotationContext({ bindings: [result] }));
    vi.mocked(fixture.client.fetchFormulaMappings).mockResolvedValue({ bindings: [current], versions: [] });
    vi.mocked(fixture.client.checkFormulaBindingUpgrade).mockRejectedValue(new TemplateAnnotationApiError(422, '公式依赖存在循环: [公式A, 公式B, 公式A]'));
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));

    await expect(
      api.upgradeFormulaBinding(current.id, {
        formulaVersionId: 'formula-version-2',
        parameterBindings: {},
        resultBindingId: result.id,
      }),
    ).rejects.toThrow('公式依赖存在循环: [公式A, 公式B, 公式A]');

    expect(api.store.status).toBe('ready');
    expect(api.store.operation).toEqual({ kind: 'error', message: '公式依赖存在循环: [公式A, 公式B, 公式A]' });
  });

  it('公式升级 confirm 响应丢失时使用同一操作码重试且不恢复结果锁', async () => {
    const fixture = createFixture();
    const oldResult = binding({ id: 'old-result', status: 'ACTIVE', controlTag: 'eln.field.old' });
    const newResult = binding({ id: 'new-result', status: 'ACTIVE', controlTag: 'eln.field.new' });
    const current = formulaBinding({ id: 'formula-binding', resultBindingId: oldResult.id, status: 'ACTIVE' });
    vi.mocked(fixture.client.fetchAnnotationContext).mockResolvedValue(annotationContext({ bindings: [oldResult, newResult] }));
    vi.mocked(fixture.client.fetchFormulaMappings).mockResolvedValue({ bindings: [current], versions: [] });
    vi.mocked(fixture.client.checkFormulaBindingUpgrade).mockResolvedValue({
      downstreamFormulaBindingIds: [],
      expectedVersions: { [current.id]: current.version },
      operationCode: 'upgrade-op',
    });
    vi.mocked(fixture.client.confirmFormulaBindingUpgrade).mockRejectedValueOnce(new TypeError('Failed to fetch')).mockResolvedValueOnce(undefined);
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));

    await api.upgradeFormulaBinding(current.id, { formulaVersionId: 'formula-version-2', parameterBindings: {}, resultBindingId: newResult.id });

    expect(fixture.client.confirmFormulaBindingUpgrade).toHaveBeenCalledTimes(2);
    expect(vi.mocked(fixture.client.confirmFormulaBindingUpgrade).mock.calls[0]?.[2]?.operationCode).toBe('upgrade-op');
    expect(vi.mocked(fixture.client.confirmFormulaBindingUpgrade).mock.calls[1]?.[2]?.operationCode).toBe('upgrade-op');
    expect(fixture.bridge.restoreFormulaResultLocks).not.toHaveBeenCalled();
    expect(api.formulaBindings[0]?.status).toBe('ACTIVE');
  });

  it('公式升级 confirm 连续响应丢失后保留结果锁并恢复可操作状态', async () => {
    const fixture = createFixture();
    const oldResult = binding({ id: 'old-result', status: 'ACTIVE', controlTag: 'eln.field.old' });
    const newResult = binding({ id: 'new-result', status: 'ACTIVE', controlTag: 'eln.field.new' });
    const current = formulaBinding({ id: 'formula-binding', resultBindingId: oldResult.id, status: 'ACTIVE' });
    vi.mocked(fixture.client.fetchAnnotationContext).mockResolvedValue(annotationContext({ bindings: [oldResult, newResult] }));
    vi.mocked(fixture.client.fetchFormulaMappings).mockResolvedValue({ bindings: [current], versions: [] });
    vi.mocked(fixture.client.checkFormulaBindingUpgrade).mockResolvedValue({
      downstreamFormulaBindingIds: [],
      expectedVersions: { [current.id]: current.version },
      operationCode: 'upgrade-op',
    });
    vi.mocked(fixture.client.confirmFormulaBindingUpgrade)
      .mockRejectedValueOnce(new TypeError('Failed to fetch'))
      .mockRejectedValueOnce(new TypeError('Failed to fetch'));
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));

    await expect(
      api.upgradeFormulaBinding(current.id, { formulaVersionId: 'formula-version-2', parameterBindings: {}, resultBindingId: newResult.id }),
    ).rejects.toThrow('待确认');

    expect(fixture.client.confirmFormulaBindingUpgrade).toHaveBeenCalledTimes(2);
    expect(vi.mocked(fixture.client.confirmFormulaBindingUpgrade).mock.calls.map((call) => call[2].operationCode)).toEqual(['upgrade-op', 'upgrade-op']);
    expect(fixture.bridge.restoreFormulaResultLocks).not.toHaveBeenCalled();
    expect(api.store.status).toBe('ready');
    expect(api.store.operation).toMatchObject({ kind: 'error', message: expect.stringContaining('待对账') });
    expect(api.formulaBindings[0]).toMatchObject({ id: current.id, resultBindingId: oldResult.id, status: 'ACTIVE' });
  });

  it('刷新会重新读取服务端公式映射以完成公式升级对账', async () => {
    const fixture = createFixture();
    const oldResult = binding({ id: 'old-result', status: 'ACTIVE', controlTag: 'eln.field.old' });
    const newResult = binding({ id: 'new-result', status: 'ACTIVE', controlTag: 'eln.field.new' });
    const current = formulaBinding({ id: 'formula-binding', resultBindingId: oldResult.id, status: 'ACTIVE' });
    const committed = formulaBinding({
      formulaVersionId: 'formula-version-2',
      id: current.id,
      resultBindingId: newResult.id,
      status: 'ACTIVE',
      version: current.version + 1,
    });
    vi.mocked(fixture.client.fetchAnnotationContext)
      .mockResolvedValueOnce(annotationContext({ bindings: [oldResult, newResult] }))
      .mockResolvedValueOnce(annotationContext({ bindings: [oldResult, newResult] }));
    vi.mocked(fixture.client.fetchFormulaMappings)
      .mockResolvedValueOnce({ bindings: [current], versions: [] })
      .mockResolvedValueOnce({ bindings: [committed], versions: [] });
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));

    await api.refresh();

    expect(fixture.client.fetchAnnotationContext).toHaveBeenCalledTimes(2);
    expect(fixture.client.fetchFormulaMappings).toHaveBeenCalledTimes(2);
    expect(api.formulaBindings[0]).toMatchObject({
      formulaVersionId: committed.formulaVersionId,
      id: committed.id,
      resultBindingId: newResult.id,
      status: 'ACTIVE',
      version: committed.version,
    });
  });

  it('刷新步骤配置时保留有效选择，并清空已失效的当前步骤', async () => {
    const fixture = createFixture();
    const initial = annotationContext({
      stepConfiguration: {
        singleStep: false,
        steps: [
          { id: 'step-1', name: '称量', order: 1 },
          { id: 'step-2', name: '溶解', order: 2 },
        ],
      },
    } as Partial<TemplateAnnotationContext>);
    const withoutSelectedStep = annotationContext({
      stepConfiguration: {
        singleStep: false,
        steps: [{ id: 'step-1', name: '称量', order: 1 }],
      },
    } as Partial<TemplateAnnotationContext>);
    vi.mocked(fixture.client.fetchAnnotationContext).mockResolvedValueOnce(initial).mockResolvedValueOnce(initial).mockResolvedValueOnce(withoutSelectedStep);
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));
    api.setCurrentStepId('step-2');

    await api.refresh();
    expect(api.currentStepId).toBe('step-2');

    await api.refresh();
    expect(api.currentStepId).toBeNull();
  });

  it('删除仍被下游公式引用的映射时不解除任何结果锁', async () => {
    const fixture = createFixture();
    const result = binding({ id: 'result-binding', status: 'ACTIVE' });
    const current = formulaBinding({ id: 'formula-binding', resultBindingId: result.id, status: 'ACTIVE' });
    vi.mocked(fixture.client.fetchAnnotationContext).mockResolvedValue(annotationContext({ bindings: [result] }));
    vi.mocked(fixture.client.fetchFormulaMappings).mockResolvedValue({ bindings: [current], versions: [] });
    vi.mocked(fixture.client.checkFormulaBindingRemoval).mockResolvedValue({
      downstreamFormulaBindingIds: ['downstream-formula'],
      expectedVersions: { [current.id]: current.version },
      operationCode: 'remove-formula',
    });
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));

    await expect(api.removeFormulaBinding(current.id)).rejects.toThrow('公式结果仍被下游公式引用');

    expect(fixture.bridge.lockFormulaResult).not.toHaveBeenCalled();
    expect(fixture.bridge.unlockFormulaResult).not.toHaveBeenCalled();
    expect(fixture.client.confirmFormulaBindingRemoval).not.toHaveBeenCalled();
  });

  it('SDK ready 先关闭修订再读取模板标注，避免写操作前打断用户选择', async () => {
    const fixture = createFixture();
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));
    expect(fixture.order).toEqual(['review', 'list', 'context']);
    expect(fixture.bridge.disableTrackRevisions).toHaveBeenCalledOnce();
    expect(fixture.client.fetchAnnotationContext).toHaveBeenCalledWith(expect.objectContaining({ ticket: 'ticket' }));
  });

  it('重复 SDK ready 复用同一个修订关闭操作', async () => {
    const fixture = createFixture();
    let completeReview!: () => void;
    vi.mocked(fixture.bridge.disableTrackRevisions).mockImplementation(
      () =>
        new Promise<void>((resolve) => {
          completeReview = resolve;
        }),
    );
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);

    fixture.ready();
    fixture.ready();
    await vi.waitFor(() => expect(fixture.bridge.disableTrackRevisions).toHaveBeenCalledOnce());
    completeReview();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));

    expect(fixture.bridge.disableTrackRevisions).toHaveBeenCalledOnce();
  });

  it('只读定位模式仅枚举并唯一定位，不创建 API 上下文或关闭修订', async () => {
    const fixture = createFixture();
    const { scope: _scope, ticket: _ticket, ...locateOptions } = validOptions;
    vi.mocked(fixture.bridge.getRuntimeOptions).mockReturnValue({ ...locateOptions, canAnnotate: false, locateControlTag: 'eln.field.orphan' });
    vi.mocked(fixture.bridge.listControls).mockResolvedValue([{ InternalId: 'orphan-control', Tag: 'eln.field.orphan' }]);
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));

    expect(fixture.bridge.selectControl).toHaveBeenCalledWith('orphan-control', expect.any(AbortSignal));
    expect(fixture.bridge.disableTrackRevisions).not.toHaveBeenCalled();
    expect(fixture.client.fetchAnnotationContext).not.toHaveBeenCalled();
    expect(fixture.client.fetchRepairContext).not.toHaveBeenCalled();
    expect(fixture.client.activateBinding).not.toHaveBeenCalled();
    expect(fixture.client.cancelBinding).not.toHaveBeenCalled();
    expect(fixture.client.checkBindingRemoval).not.toHaveBeenCalled();
    expect(fixture.client.checkBindingVersionUpgrade).not.toHaveBeenCalled();
    expect(fixture.client.checkReferencePromotion).not.toHaveBeenCalled();
    expect(fixture.client.confirmBindingRemoval).not.toHaveBeenCalled();
    expect(fixture.client.confirmBindingVersionUpgrade).not.toHaveBeenCalled();
    expect(fixture.client.confirmReferencePromotion).not.toHaveBeenCalled();
    expect(fixture.client.confirmRepair).not.toHaveBeenCalled();
    expect(fixture.client.reservePrimaryBinding).not.toHaveBeenCalled();
    expect(fixture.client.reservePrivateField).not.toHaveBeenCalled();
    expect(fixture.client.reserveReferenceBinding).not.toHaveBeenCalled();
    expect(fixture.client.confirmVersionCopy).not.toHaveBeenCalled();

    await api.refresh();
    expect(fixture.bridge.listControls).toHaveBeenCalledTimes(2);
    expect(fixture.bridge.selectControl).toHaveBeenCalledTimes(2);
    expect(fixture.client.fetchAnnotationContext).not.toHaveBeenCalled();
  });

  it('纯定位 initialize 的枚举在 dispose 后返回时不再定位控件', async () => {
    const fixture = createFixture();
    let resolveControls!: (controls: OfficeContentControl[]) => void;
    vi.mocked(fixture.bridge.getRuntimeOptions).mockReturnValue({ ...validOptions, canAnnotate: false, locateControlTag: 'eln.field.orphan' });
    vi.mocked(fixture.bridge.listControls).mockReturnValue(
      new Promise((done) => {
        resolveControls = done;
      }),
    );
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(fixture.bridge.listControls).toHaveBeenCalledOnce());

    api.dispose();
    resolveControls([{ InternalId: 'orphan-control', Tag: 'eln.field.orphan' }]);
    await new Promise((done) => setTimeout(done, 0));

    expect(fixture.bridge.selectControl).not.toHaveBeenCalled();
  });

  it.each([
    ['不存在', [], '不存在'],
    [
      '出现多次',
      [
        { InternalId: 'first', Tag: 'eln.field.orphan' },
        { InternalId: 'second', Tag: 'eln.field.orphan' },
      ],
      '出现多次',
    ],
  ])('只读定位刷新目标%s时只报告本地诊断', async (_label, refreshControls, diagnostic) => {
    const fixture = createFixture();
    vi.mocked(fixture.bridge.getRuntimeOptions).mockReturnValue({ ...validOptions, canAnnotate: false, locateControlTag: 'eln.field.orphan' });
    vi.mocked(fixture.bridge.listControls)
      .mockResolvedValueOnce([{ InternalId: 'orphan-control', Tag: 'eln.field.orphan' }])
      .mockResolvedValueOnce(refreshControls);
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));

    await expect(api.refresh()).rejects.toThrow(diagnostic);

    expect(api.store.operation.message).toContain(diagnostic);
    expect(fixture.client.fetchAnnotationContext).not.toHaveBeenCalled();
    expect(fixture.client.fetchRepairContext).not.toHaveBeenCalled();
  });

  it.each([
    ['不存在', [], '不存在'],
    [
      '出现多次',
      [
        { InternalId: 'first', Tag: 'eln.field.orphan' },
        { InternalId: 'second', Tag: 'eln.field.orphan' },
      ],
      '出现多次',
    ],
  ])('只读定位目标%s时明确失败', async (_label, controls, diagnostic) => {
    const fixture = createFixture();
    vi.mocked(fixture.bridge.getRuntimeOptions).mockReturnValue({ ...validOptions, canAnnotate: false, locateControlTag: 'eln.field.orphan' });
    vi.mocked(fixture.bridge.listControls).mockResolvedValue(controls);
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('error'));

    expect(api.store.operation.message).toContain(diagnostic);
    expect(fixture.bridge.selectControl).not.toHaveBeenCalled();
    expect(fixture.client.fetchAnnotationContext).not.toHaveBeenCalled();
    expect(fixture.client.fetchRepairContext).not.toHaveBeenCalled();
  });

  it('固定执行 reserve→插入→重新枚举确认 0→1→activate', async () => {
    const fixture = createFixture();
    vi.mocked(fixture.bridge.listControls)
      .mockResolvedValueOnce([])
      .mockResolvedValueOnce([])
      .mockResolvedValueOnce([{ Tag: 'eln.field.result', BindingId: 'binding' }]);
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));
    fixture.order.splice(0);
    await api.insertSelectedField(option);
    expect(fixture.order).toEqual(['reserve', 'insert', 'activate']);
    const calls = [
      vi.mocked(fixture.client.reservePrimaryBinding).mock.invocationCallOrder[0]!,
      vi.mocked(fixture.bridge.listControls).mock.invocationCallOrder[1]!,
      vi.mocked(fixture.bridge.insertBinding).mock.invocationCallOrder[0]!,
      vi.mocked(fixture.bridge.listControls).mock.invocationCallOrder[2]!,
      vi.mocked(fixture.client.activateBinding).mock.invocationCallOrder[0]!,
    ];
    expect(calls).toEqual([...calls].sort((left, right) => left - right));
  });

  it('复选框标注携带选择组规则，改组和改规则只更新元数据而不写 DOCX', async () => {
    const fixture = createFixture();
    const checkbox = { ...option, fieldCode: 'passed_new', fieldId: 'passed-new-field', fieldVersionId: 'passed-new-version', type: 'CHECKBOX' as const };
    const active = binding({
      checkboxGroupCode: 'inspection_result',
      controlTag: 'eln.field.passed',
      controlType: 'CHECKBOX',
      fieldCode: 'passed',
      fieldId: 'passed-field',
      fieldVersionId: 'passed-version',
      status: 'ACTIVE',
      version: 4,
    });
    vi.mocked(fixture.client.fetchAnnotationContext).mockResolvedValue(
      annotationContext({
        bindings: [active],
        checkboxGroups: [{ groupCode: 'inspection_result', maxSelected: 2, minSelected: 1, version: 3 }],
      }),
    );
    vi.mocked(fixture.bridge.listControls).mockResolvedValue([{ BindingId: active.id, Tag: active.controlTag }]);
    vi.mocked(fixture.client.reservePrimaryBinding).mockResolvedValue(binding({
      checkboxGroupCode: 'inspection_result',
      controlTag: 'eln.field.passed_new',
      controlType: 'CHECKBOX',
      fieldCode: 'passed_new',
      id: 'new-checkbox',
    }));
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));

    vi.mocked(fixture.bridge.listControls)
      .mockResolvedValueOnce([{ BindingId: active.id, Tag: active.controlTag }])
      .mockResolvedValueOnce([
        { BindingId: active.id, Tag: active.controlTag },
        { BindingId: 'new-checkbox', Tag: 'eln.field.passed_new' },
      ]);
    await api.insertSelectedField(checkbox, { groupCode: 'inspection_result', maxSelected: 2, minSelected: 1 });
    expect(fixture.client.reservePrimaryBinding).toHaveBeenCalledWith(
      expect.anything(),
      expect.objectContaining({
        checkboxGroupCode: 'inspection_result',
        checkboxGroupMaxSelected: 2,
        checkboxGroupMinSelected: 1,
      }),
    );
    expect(api.checkboxGroups).toEqual([{ groupCode: 'inspection_result', maxSelected: 2, minSelected: 1, version: 3 }]);

    vi.mocked(fixture.bridge.insertBinding).mockClear();
    await api.changeCheckboxGroup(active.id, { groupCode: 'review_result', maxSelected: null, minSelected: 2 }, active.version);
    expect(fixture.client.changeCheckboxGroup).toHaveBeenCalledWith(expect.anything(), active.id, {
      checkboxGroupCode: 'review_result',
      checkboxGroupMaxSelected: null,
      checkboxGroupMinSelected: 2,
      expectedVersion: 4,
      operationCode: expect.any(String),
    });
    await api.changeCheckboxGroupRule('inspection_result', 0, 3, 3);
    expect(fixture.client.changeCheckboxGroupRule).toHaveBeenCalledWith(expect.anything(), 'inspection_result', {
      expectedVersion: 3,
      maxSelected: 3,
      minSelected: 0,
      operationCode: expect.any(String),
    });
    expect(fixture.bridge.insertBinding).not.toHaveBeenCalled();
    expect(api.bindings.find((item) => item.id === active.id)).toMatchObject({ checkboxGroupCode: 'review_result', version: 5 });
    expect(api.checkboxGroups).toContainEqual({ groupCode: 'inspection_result', maxSelected: 3, minSelected: 0, version: 4 });
  });

  it('先加载来源模板，再按所选模板加载字段候选', async () => {
    const fixture = createFixture();
    const sourceTemplate: CrossTemplateSourceTemplate = {
      sourceTemplateCode: 'SOURCE-TPL',
      sourceTemplateId: 'source-template',
    };
    vi.mocked(fixture.client.fetchCrossTemplateSourceTemplates).mockResolvedValue([sourceTemplate]);
    vi.mocked(fixture.client.fetchCrossTemplateScalarCandidates).mockResolvedValue([crossTemplateCandidate]);
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));

    await api.loadCrossTemplateSourceTemplates();
    expect(api.crossTemplateSourceTemplates).toEqual([sourceTemplate]);
    expect(api.crossTemplateScalarCandidates).toEqual([]);

    await api.loadCrossTemplateScalarCandidates('source-template');
    expect(fixture.client.fetchCrossTemplateScalarCandidates).toHaveBeenCalledWith(
      expect.anything(),
      'source-template',
    );
    expect(api.crossTemplateScalarCandidates).toEqual([crossTemplateCandidate]);
  });

  it('跨模板字段值走独立 reserve，并以锁定的 PRIMARY 控件完成 activate', async () => {
    const fixture = createFixture();
    const bridge = fixture.bridge as OnlyOfficeBridge & { insertReadOnlyBinding: ReturnType<typeof vi.fn> };
    bridge.insertReadOnlyBinding = vi.fn(async () => {
      fixture.order.push('insert-cross-template');
    });
    vi.mocked(fixture.bridge.listControls)
      .mockResolvedValueOnce([])
      .mockResolvedValueOnce([])
      .mockResolvedValueOnce([{ BindingId: 'cross-binding', Lock: 'sdtContentLocked', Tag: 'eln.field.result' }]);
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));
    fixture.order.splice(0);

    await api.insertCrossTemplateScalar(crossTemplateCandidate);

    expect(fixture.order).toEqual(['reserve-cross-template', 'insert-cross-template', 'activate']);
    expect(fixture.client.reserveCrossTemplateScalarBinding).toHaveBeenCalledWith(expect.anything(), {
      operationCode: expect.any(String),
      repeatRequest: false,
      sourceFieldId: 'source-field',
      sourceFieldVersionId: 'source-version',
      sourceTemplateCode: 'SOURCE-TPL',
      sourceTemplateId: 'source-template',
      stepId: 'step-1',
    });
    expect(fixture.client.reservePrimaryBinding).not.toHaveBeenCalled();
    expect(bridge.insertReadOnlyBinding).toHaveBeenCalledWith(
      expect.objectContaining({ controlRole: 'PRIMARY', controlTag: 'eln.field.result', readOnly: true, valueSource: 'CROSS_TEMPLATE' }),
      expect.any(AbortSignal),
    );
  });

  it('跨模板 activate 响应丢失但服务端已 ACTIVE 时保留只读控件', async () => {
    const fixture = createFixture();
    const active = binding({ id: 'cross-binding', readOnly: true, status: 'ACTIVE', valueSource: 'CROSS_TEMPLATE' });
    vi.mocked(fixture.bridge.listControls)
      .mockResolvedValueOnce([])
      .mockResolvedValueOnce([])
      .mockResolvedValueOnce([{ BindingId: active.id, InternalId: 'cross-control', Lock: 'sdtContentLocked', Tag: active.controlTag }]);
    vi.mocked(fixture.client.activateBinding).mockRejectedValue(new TypeError('Failed to fetch'));
    vi.mocked(fixture.client.fetchAnnotationContext)
      .mockResolvedValueOnce(annotationContext())
      .mockResolvedValueOnce(annotationContext({ bindings: [active] }));
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));

    await api.insertCrossTemplateScalar(crossTemplateCandidate);

    expect(fixture.bridge.deleteControl).not.toHaveBeenCalled();
    expect(fixture.client.cancelBinding).not.toHaveBeenCalled();
    expect(api.bindings).toContainEqual(expect.objectContaining({ id: active.id, status: 'ACTIVE' }));
  });

  it('跨模板 activate 结果无法查明时保留控件等待对账', async () => {
    const fixture = createFixture();
    vi.mocked(fixture.bridge.listControls)
      .mockResolvedValueOnce([])
      .mockResolvedValueOnce([])
      .mockResolvedValueOnce([{ BindingId: 'cross-binding', InternalId: 'cross-control', Lock: 'sdtContentLocked', Tag: 'eln.field.result' }]);
    vi.mocked(fixture.client.activateBinding).mockRejectedValue(new TypeError('Failed to fetch'));
    vi.mocked(fixture.client.fetchAnnotationContext)
      .mockResolvedValueOnce(annotationContext())
      .mockRejectedValueOnce(new TypeError('Failed to fetch'));
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));

    await expect(api.insertCrossTemplateScalar(crossTemplateCandidate)).rejects.toThrow('待对账');

    expect(fixture.bridge.deleteControl).not.toHaveBeenCalled();
    expect(fixture.client.cancelBinding).not.toHaveBeenCalled();
    expect(api.store.operation).toMatchObject({ kind: 'error', message: expect.stringContaining('待对账') });
  });

  it('跨模板 activate 结果查询返回过期 Ticket 时清空上下文且停止补偿写', async () => {
    const fixture = createFixture();
    vi.mocked(fixture.bridge.listControls)
      .mockResolvedValueOnce([])
      .mockResolvedValueOnce([])
      .mockResolvedValueOnce([{ BindingId: 'cross-binding', InternalId: 'cross-control', Lock: 'sdtContentLocked', Tag: 'eln.field.result' }]);
    vi.mocked(fixture.client.activateBinding).mockRejectedValue(new TypeError('Failed to fetch'));
    vi.mocked(fixture.client.fetchAnnotationContext)
      .mockResolvedValueOnce(annotationContext())
      .mockRejectedValueOnce(new TemplateAnnotationApiError(401, 'Ticket 已过期'));
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));

    await expect(api.insertCrossTemplateScalar(crossTemplateCandidate)).rejects.toThrow('Ticket 已过期');

    expect(fixture.bridge.deleteControl).not.toHaveBeenCalled();
    expect(fixture.client.cancelBinding).not.toHaveBeenCalled();
    expect(api.contextStore.context).toBeNull();
    expect(api.store.status).toBe('error');
  });

  it('跨模板插入命令已落控件但响应失败时删除控件后 cancel', async () => {
    const fixture = createFixture();
    vi.mocked(fixture.bridge.insertReadOnlyBinding).mockRejectedValue(new Error('OnlyOffice command response lost'));
    vi.mocked(fixture.bridge.listControls)
      .mockResolvedValueOnce([])
      .mockResolvedValueOnce([])
      .mockResolvedValueOnce([{ InternalId: 'cross-control', Lock: 'sdtContentLocked', Tag: 'eln.field.result' }]);
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));

    await expect(api.insertCrossTemplateScalar(crossTemplateCandidate)).rejects.toThrow('response lost');

    expect(fixture.bridge.deleteControl).toHaveBeenCalledWith('cross-control');
    expect(fixture.bridge.deleteControl).toHaveBeenCalledBefore(fixture.client.cancelBinding as ReturnType<typeof vi.fn>);
  });

  it('多步骤模板选择当前步骤后才允许标注，并在 reserve 中携带步骤归属', async () => {
    const fixture = createFixture();
    vi.mocked(fixture.client.fetchAnnotationContext).mockResolvedValue(
      annotationContext({
        stepConfiguration: {
          singleStep: false,
          steps: [
            { id: 'step-1', name: '称量', order: 1 },
            { id: 'step-2', name: '溶解', order: 2 },
          ],
        },
      } as Partial<TemplateAnnotationContext>),
    );
    vi.mocked(fixture.bridge.listControls)
      .mockResolvedValueOnce([])
      .mockResolvedValueOnce([])
      .mockResolvedValueOnce([{ Tag: 'eln.field.result', BindingId: 'binding' }]);
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));

    expect(api.currentStepId).toBeNull();
    await expect(api.insertSelectedField(option)).rejects.toThrow('多步骤模板必须选择当前步骤');

    api.setCurrentStepId('step-2');
    await api.insertSelectedField(option);

    expect(fixture.client.reservePrimaryBinding).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ stepId: 'step-2' }));
  });

  it('重复行 reserve 携带模板版本内的行数约束', async () => {
    const fixture = createFixture();
    const repeat = binding({
      controlRole: 'REPEAT',
      controlTag: 'eln.repeat.results.prototype.result',
      repeatGroupCode: 'results',
      repeatPrototypeRowId: 'prototype',
    });
    vi.mocked(fixture.client.reservePrimaryBinding).mockResolvedValue(repeat);
    vi.mocked(fixture.bridge.listControls)
      .mockResolvedValueOnce([])
      .mockResolvedValueOnce([])
      .mockResolvedValueOnce([{ Tag: repeat.controlTag, BindingId: repeat.id }]);
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));

    await api.insertRepeatField(option, { groupCode: 'results', maxRows: 20, minRows: 2 });

    expect(fixture.client.reservePrimaryBinding).toHaveBeenCalledWith(
      expect.anything(),
      expect.objectContaining({
        maxRows: 20,
        minRows: 2,
        repeatGroupCode: 'results',
        repeatPrototypeRowId: 'prototype',
        repeatRequest: true,
        stepId: 'step-1',
      }),
    );
  });

  it('已有重复组后续字段继承组原步骤，不随当前步骤变化', async () => {
    const fixture = createFixture();
    const repeat = binding({
      controlRole: 'REPEAT',
      controlTag: 'eln.repeat.results.prototype.result',
      repeatGroupCode: 'results',
      repeatPrototypeRowId: 'prototype',
      stepId: 'step-1',
    });
    vi.mocked(fixture.client.fetchAnnotationContext).mockResolvedValue(
      annotationContext({
        repeatGroups: [{ groupCode: 'results', maxRows: 20, minRows: 2, stepId: 'step-1' }],
        stepConfiguration: {
          singleStep: false,
          steps: [
            { id: 'step-1', name: '称量', order: 1 },
            { id: 'step-2', name: '溶解', order: 2 },
          ],
        },
      } as Partial<TemplateAnnotationContext>),
    );
    vi.mocked(fixture.client.reservePrimaryBinding).mockResolvedValue(repeat);
    vi.mocked(fixture.bridge.listControls)
      .mockResolvedValueOnce([])
      .mockResolvedValueOnce([])
      .mockResolvedValueOnce([{ Tag: repeat.controlTag, BindingId: repeat.id }]);
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));
    api.setCurrentStepId('step-2');

    await api.insertRepeatField(option, { groupCode: 'results', maxRows: 20, minRows: 2 });

    expect(fixture.client.reservePrimaryBinding).toHaveBeenCalledWith(
      expect.anything(),
      expect.objectContaining({ repeatGroupCode: 'results', stepId: 'step-1' }),
    );
  });

  it('已有重复组缺少步骤归属时拒绝标注，不回退到当前步骤', async () => {
    const fixture = createFixture();
    vi.mocked(fixture.client.fetchAnnotationContext).mockResolvedValue(
      annotationContext({
        repeatGroups: [{ groupCode: 'results', maxRows: 20, minRows: 2 }],
        stepConfiguration: {
          singleStep: false,
          steps: [
            { id: 'step-1', name: '称量', order: 1 },
            { id: 'step-2', name: '溶解', order: 2 },
          ],
        },
      } as Partial<TemplateAnnotationContext>),
    );
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));
    api.setCurrentStepId('step-2');

    await expect(api.insertRepeatField(option, { groupCode: 'results', maxRows: 20, minRows: 2 })).rejects.toThrow('重复组步骤归属无效');

    expect(fixture.client.reservePrimaryBinding).not.toHaveBeenCalled();
  });

  it('重复行 reserve 拒绝越界行数约束', async () => {
    const fixture = createFixture();
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));

    await expect(
      api.insertRepeatField(option, {
        groupCode: 'results',
        maxRows: 20,
        minRows: -1,
      }),
    ).rejects.toThrow('重复组行数约束无效');
    await expect(
      api.insertRepeatField(option, {
        groupCode: 'results',
        maxRows: 4,
        minRows: 5,
      }),
    ).rejects.toThrow('重复组行数约束无效');
    await expect(
      api.insertRepeatField(option, {
        groupCode: 'results',
        maxRows: 1001,
        minRows: 0,
      }),
    ).rejects.toThrow('重复组行数约束无效');
    expect(fixture.client.reservePrimaryBinding).not.toHaveBeenCalled();
  });

  it('插入失败只取消仍为 PENDING 的绑定，cancel 失败不覆盖原始错误', async () => {
    const fixture = createFixture();
    vi.mocked(fixture.bridge.insertBinding).mockRejectedValue(new Error('文档写入失败'));
    vi.mocked(fixture.client.cancelBinding).mockRejectedValue(new Error('补偿失败'));
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));
    await expect(api.insertSelectedField(option)).rejects.toThrow('文档写入失败');
    expect(fixture.client.cancelBinding).toHaveBeenCalledWith(expect.anything(), 'binding', expect.objectContaining({ reason: '文档写入失败' }));

    vi.mocked(fixture.client.reservePrimaryBinding).mockResolvedValue(binding({ status: 'ACTIVE' }));
    await expect(api.insertSelectedField(option)).rejects.toThrow('文档写入失败');
    expect(fixture.client.cancelBinding).toHaveBeenCalledTimes(1);
  });

  it('私有字段创建激活后刷新字段上下文，使公式映射可立即使用', async () => {
    const fixture = createFixture();
    const privateBinding = binding({ checkboxGroupCode: 'inspection_result', controlType: 'CHECKBOX', fieldId: 'private', fieldVersionId: 'private-version' });
    const updatedContext = annotationContext({
      bindings: [{ ...privateBinding, status: 'ACTIVE' }],
      fields: [
        {
          beizhu: null,
          bizEnabled: 'enabled',
          dangqianZiduanBanbenId: null,
          id: 'private',
          mubanBianhao: 'TMP',
          versions: [
            {
              banbenHao: 1,
              binding: { read: { property: null, source: 'NONE' }, write: { target: 'ELN_RECORD' } },
              bizStatus: 'PRE_RELEASE',
              chuangjianShijian: null,
              danwei: null,
              gengxinShijian: null,
              id: 'private-version',
              kongjianPeizhi: { defaultChecked: false, placeholder: '' },
              piciMoshi: 'RECORD_PRIVATE',
              shifouBitian: 0,
              shujuLeixing: 'CHECKBOX',
              ziduanDingyiId: 'private',
              ziduanMingcheng: 'P3验收参数A',
              zuofeiYuanyin: null,
            },
          ],
          ziduanBianma: 'p3_parameter_a',
          zuoyongFanwei: 'TEMPLATE',
        },
      ],
    });
    vi.mocked(fixture.client.reservePrivateField).mockImplementation(async () => {
      fixture.order.push('reserve-private');
      return privateBinding;
    });
    vi.mocked(fixture.client.fetchAnnotationContext)
      .mockImplementationOnce(async () => {
        fixture.order.push('context');
        return annotationContext();
      })
      .mockImplementationOnce(async () => {
        fixture.order.push('context');
        return updatedContext;
      });
    vi.mocked(fixture.bridge.listControls)
      .mockResolvedValueOnce([])
      .mockResolvedValueOnce([])
      .mockResolvedValueOnce([{ Tag: 'eln.field.result', BindingId: 'binding' }]);
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));
    fixture.order.splice(0);
    await api.createPrivateField({
      checkboxGroupCode: 'inspection_result',
      kongjianPeizhi: { defaultChecked: false, placeholder: '' },
      shujuLeixing: 'CHECKBOX',
      ziduanMingcheng: 'P3验收参数A',
    });
    expect(fixture.order).toEqual(['reserve-private', 'insert', 'activate', 'context']);
    expect(fixture.client.reservePrivateField).toHaveBeenCalledWith(
      expect.anything(),
      expect.objectContaining({ checkboxGroupCode: 'inspection_result', stepId: 'step-1' }),
    );
    expect(fixture.bridge.listControls).toHaveBeenCalledTimes(3);
    expect(api.fields).toEqual(expect.arrayContaining([expect.objectContaining({ fieldVersionId: 'private-version', label: 'P3验收参数A', type: 'CHECKBOX' })]));
  });

  it('引用使用后端 eln.ref Tag，在插入和枚举后激活', async () => {
    const fixture = createFixture();
    const primary = binding({ status: 'ACTIVE', version: 2 });
    vi.mocked(fixture.client.fetchAnnotationContext).mockResolvedValue(annotationContext({ bindings: [primary] }));
    vi.mocked(fixture.bridge.listControls)
      .mockResolvedValueOnce([{ Tag: primary.controlTag, Text: '12.3', BindingId: primary.id }])
      .mockResolvedValueOnce([{ Tag: primary.controlTag }])
      .mockResolvedValueOnce([{ Tag: primary.controlTag }, { Tag: 'eln.ref.result.abc', BindingId: 'reference' }]);
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));
    fixture.order.splice(0);
    await api.insertReference('binding');
    expect(fixture.bridge.insertReference).toHaveBeenCalledWith(
      expect.objectContaining({ controlTag: 'eln.ref.result.abc' }),
      primary.controlTag,
      expect.any(AbortSignal),
    );
    expect(fixture.order).toEqual(['reserve-reference', 'insert-reference', 'activate']);
    expect(fixture.bridge.listControls).toHaveBeenCalledTimes(3);
  });

  it('升版复制 PENDING 枚举全部控件后一次 confirm，成功后才显示 ACTIVE', async () => {
    const fixture = createFixture();
    const pending = binding({ id: 'copy', version: 4 });
    vi.mocked(fixture.client.fetchAnnotationContext).mockResolvedValue(
      annotationContext({ bindings: [pending], copiedPendingBindingIds: ['copy'], upgradeOperationCode: 'upgrade-1' } as any),
    );
    vi.mocked(fixture.bridge.listControls).mockResolvedValue([{ Tag: pending.controlTag, BindingId: pending.id }]);
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));
    expect(fixture.client.confirmVersionCopy).toHaveBeenCalledWith(expect.anything(), {
      controls: [{ bindingId: 'copy', controlTag: 'eln.field.result' }],
      expectedVersions: { copy: 4 },
      upgradeOperationCode: 'upgrade-1',
    });
    const snapshotOptions = vi.mocked(fixture.bridge.snapshotControls).mock.calls[0]?.[2];
    expect([...snapshotOptions!.inheritedFormulaLockBindingIds!]).toEqual([]);
    expect(api.bindings[0]?.status).toBe('ACTIVE');
  });

  it('升版字段确认使用服务端公式锁白名单，并刷新上下文确认随后生成的公式复制', async () => {
    const fixture = createFixture();
    const pending = binding({ id: 'locked-copy', status: 'PENDING', version: 4 });
    const reference = binding({
      controlRole: 'REFERENCE',
      controlTag: 'eln.ref.result.copy',
      id: 'locked-reference-copy',
      primaryBindingId: pending.id,
      status: 'PENDING',
      version: 3,
    });
    const copiedFormula = formulaBinding({
      id: 'copied-formula',
      operationCode: 'upgrade-locked',
      resultBindingId: pending.id,
      status: 'PENDING',
      version: 2,
    });
    vi.mocked(fixture.client.fetchAnnotationContext).mockResolvedValue(
      annotationContext({
        bindings: [pending, reference],
        copiedPendingBindingIds: [pending.id, reference.id],
        upgradeOperationCode: 'upgrade-locked',
      } as any),
    );
    vi.mocked(fixture.client.fetchFormulaMappings)
      .mockResolvedValueOnce({
        bindings: [],
        inheritedFormulaLockBindingIds: [pending.id, reference.id],
        versions: [],
      })
      .mockResolvedValueOnce({
        bindings: [copiedFormula],
        copiedPendingBindingIds: [copiedFormula.id],
        upgradeOperationCode: 'upgrade-locked',
        versions: [],
      });
    vi.mocked(fixture.bridge.listControls).mockResolvedValue([
      {
        BindingId: pending.id,
        Lock: 'sdtContentLocked',
        Tag: pending.controlTag,
      },
      {
        BindingId: reference.id,
        Lock: 'sdtContentLocked',
        Tag: reference.controlTag,
      },
    ]);
    vi.mocked(fixture.bridge.snapshotControls).mockImplementation((_bindings, _controls, options) => {
      if (!options?.inheritedFormulaLockBindingIds?.has(pending.id) || !options.inheritedFormulaLockBindingIds.has(reference.id))
        throw new Error('复制公式结果锁未按绑定显式允许');
      return [
        { bindingId: pending.id, controlTag: pending.controlTag },
        { bindingId: reference.id, controlTag: reference.controlTag },
      ];
    });
    vi.mocked(fixture.bridge.isFormulaResultLocked).mockResolvedValue(true);
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();

    await vi.waitFor(() => expect(api.store.status).toBe('ready'));
    expect(fixture.client.fetchFormulaMappings).toHaveBeenCalledTimes(2);
    expect(fixture.client.confirmVersionCopy).toHaveBeenCalledOnce();
    expect(fixture.client.confirmFormulaVersionCopy).toHaveBeenCalledOnce();
    expect(api.bindings.every((item) => item.status === 'ACTIVE')).toBe(true);
    expect(api.formulaBindings[0]?.status).toBe('ACTIVE');
  });

  it('复制 PENDING 缺少后端升级操作码时明确失败且不猜测', async () => {
    const fixture = createFixture();
    vi.mocked(fixture.client.fetchAnnotationContext).mockResolvedValue(annotationContext({ bindings: [binding()], copiedPendingBindingIds: ['binding'] }));
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('error'));
    expect(api.store.operation.message).toContain('缺少升版操作码');
    expect(fixture.client.confirmVersionCopy).not.toHaveBeenCalled();
  });

  it('普通 PENDING 不触发复制确认，且无升级操作码时保持 PENDING', async () => {
    const fixture = createFixture();
    const ordinary = binding({ id: 'ordinary' });
    vi.mocked(fixture.client.fetchAnnotationContext).mockResolvedValue(annotationContext({ bindings: [ordinary] }));
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));

    expect(fixture.client.confirmVersionCopy).not.toHaveBeenCalled();
    expect(api.bindings).toEqual([ordinary]);
  });

  it('混合 PENDING 只确认后端标记的复制绑定', async () => {
    const fixture = createFixture();
    const ordinary = binding({ id: 'ordinary', controlTag: 'eln.field.ordinary' });
    const copied = binding({ id: 'copied', controlTag: 'eln.field.copied', version: 4 });
    vi.mocked(fixture.client.fetchAnnotationContext).mockResolvedValue(
      annotationContext({
        bindings: [ordinary, copied],
        copiedPendingBindingIds: ['copied'],
        upgradeOperationCode: 'upgrade-1',
      } as any),
    );
    vi.mocked(fixture.bridge.listControls).mockResolvedValue([
      { BindingId: ordinary.id, Tag: ordinary.controlTag },
      { BindingId: copied.id, Tag: copied.controlTag },
    ]);
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));

    expect(fixture.client.confirmVersionCopy).toHaveBeenCalledWith(expect.anything(), {
      controls: [{ bindingId: copied.id, controlTag: copied.controlTag }],
      expectedVersions: { copied: 4 },
      upgradeOperationCode: 'upgrade-1',
    });
    expect(api.bindings.find((item) => item.id === ordinary.id)?.status).toBe('PENDING');
    expect(api.bindings.find((item) => item.id === copied.id)?.status).toBe('ACTIVE');
  });

  it('复制 PENDING 集合指向非 PENDING 绑定时拒绝不一致上下文', async () => {
    const fixture = createFixture();
    const active = binding({ id: 'active', status: 'ACTIVE' });
    vi.mocked(fixture.client.fetchAnnotationContext).mockResolvedValue(
      annotationContext({
        bindings: [active],
        copiedPendingBindingIds: [active.id],
      } as any),
    );
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('error'));

    expect(api.store.operation.message).toContain('复制绑定上下文不一致');
    expect(fixture.client.confirmVersionCopy).not.toHaveBeenCalled();
  });

  it('dispose 后 reserve 首轮枚举返回时不写 DOCX 或使用新会话 Ticket', async () => {
    const fixture = createFixture();
    let resolveControls!: (controls: OfficeContentControl[]) => void;
    vi.mocked(fixture.bridge.listControls)
      .mockResolvedValueOnce([])
      .mockReturnValueOnce(
        new Promise((done) => {
          resolveControls = done;
        }),
      );
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));

    const operation = api.insertSelectedField(option);
    await vi.waitFor(() => expect(fixture.bridge.listControls).toHaveBeenCalledTimes(2));
    api.dispose();
    const freshFixture = createFixture();
    vi.mocked(freshFixture.bridge.getRuntimeOptions).mockReturnValue({ ...validOptions, ticket: 'fresh-ticket' });
    const freshApi = useTemplateAnnotator(freshFixture.bridge, freshFixture.client);
    freshFixture.ready();
    await vi.waitFor(() => expect(freshApi.store.status).toBe('ready'));
    resolveControls([]);
    await operation;

    expect(fixture.bridge.insertBinding).not.toHaveBeenCalled();
    expect(fixture.client.activateBinding).not.toHaveBeenCalled();
    expect(fixture.client.cancelBinding).not.toHaveBeenCalled();
  });

  it('dispose 后 DOCX 插入返回时不继续枚举、激活、取消或删除，PENDING 残留留给重开对账', async () => {
    const fixture = createFixture();
    let resolveInsert!: () => void;
    vi.mocked(fixture.bridge.listControls)
      .mockResolvedValueOnce([])
      .mockResolvedValueOnce([])
      .mockResolvedValueOnce([{ Tag: 'eln.field.result', BindingId: 'binding' }]);
    vi.mocked(fixture.bridge.insertBinding).mockReturnValue(
      new Promise((done) => {
        resolveInsert = done;
      }),
    );
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));

    const operation = api.insertSelectedField(option);
    await vi.waitFor(() => expect(fixture.bridge.insertBinding).toHaveBeenCalledOnce());
    api.dispose();
    resolveInsert();
    await operation;

    expect(fixture.bridge.listControls).toHaveBeenCalledTimes(2);
    expect(fixture.client.activateBinding).not.toHaveBeenCalled();
    expect(fixture.client.cancelBinding).not.toHaveBeenCalled();
    expect(fixture.bridge.deleteControl).not.toHaveBeenCalled();
  });

  it('dispose 后 repair-context 返回时不继续插入或确认', async () => {
    const fixture = createFixture();
    const active = binding({ status: 'ACTIVE', version: 2 });
    let resolveRepair!: (value: TemplateRepairContext) => void;
    vi.mocked(fixture.client.fetchAnnotationContext).mockResolvedValue(annotationContext({ bindings: [active] }));
    vi.mocked(fixture.bridge.listControls)
      .mockResolvedValueOnce([])
      .mockResolvedValueOnce([])
      .mockResolvedValueOnce([{ Tag: active.controlTag, BindingId: active.id }]);
    vi.mocked(fixture.client.fetchRepairContext).mockReturnValue(
      new Promise((done) => {
        resolveRepair = done;
      }),
    );
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));

    const operation = api.repairBindingId(active.id);
    await vi.waitFor(() => expect(fixture.client.fetchRepairContext).toHaveBeenCalledOnce());
    api.dispose();
    resolveRepair({ ...check(), binding: active });
    await operation;

    expect(fixture.bridge.insertBinding).not.toHaveBeenCalled();
    expect(fixture.client.confirmRepair).not.toHaveBeenCalled();
  });

  it('dispose 后拒绝新的独占操作', async () => {
    const fixture = createFixture();
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    api.dispose();

    await expect(api.refresh()).rejects.toThrow('插件已关闭');
  });

  it('公开定位向 bridge 传递实例中止信号', async () => {
    const fixture = createFixture();
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));

    await api.select('control-1');

    expect(fixture.bridge.selectControl).toHaveBeenCalledWith('control-1', expect.any(AbortSignal));
  });

  it('repairBindingId 启动时 Tag 缺失只加载修复上下文，点击恢复后才插入并 confirm', async () => {
    const fixture = createFixture();
    const active = binding({ status: 'ACTIVE', version: 2 });
    vi.mocked(fixture.client.fetchAnnotationContext).mockResolvedValue(annotationContext({ bindings: [active] }));
    vi.mocked(fixture.bridge.listControls)
      .mockResolvedValueOnce([])
      .mockResolvedValueOnce([])
      .mockResolvedValueOnce([{ Tag: active.controlTag, BindingId: active.id }]);
    vi.mocked(fixture.bridge.getRuntimeOptions).mockReturnValue({ ...validOptions, repairBindingId: 'binding' });
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));

    expect(fixture.client.fetchRepairContext).toHaveBeenCalledWith(expect.anything(), 'binding');
    expect(fixture.bridge.insertBinding).not.toHaveBeenCalled();
    expect(fixture.client.confirmRepair).not.toHaveBeenCalled();
    expect(api.repairMode).toBe(true);
    expect(api.pendingRepairBindingId).toBe('binding');

    await api.repairBindingId('binding');
    expect(fixture.bridge.insertBinding).toHaveBeenCalledWith(expect.objectContaining({ id: 'binding' }), expect.any(AbortSignal));
    expect(fixture.client.confirmRepair).toHaveBeenCalledWith(expect.anything(), 'binding', expect.objectContaining({ operationCode: 'check-op' }));
  });

  it('点击恢复后立即进入处理中状态', async () => {
    const fixture = createFixture();
    const active = binding({ status: 'ACTIVE', version: 2 });
    let resolveControls!: (controls: OfficeContentControl[]) => void;
    vi.mocked(fixture.client.fetchAnnotationContext).mockResolvedValue(annotationContext({ bindings: [active] }));
    vi.mocked(fixture.bridge.getRuntimeOptions).mockReturnValue({ ...validOptions, repairBindingId: active.id });
    vi.mocked(fixture.bridge.listControls)
      .mockResolvedValueOnce([])
      .mockImplementationOnce(
        () =>
          new Promise((resolve) => {
            resolveControls = resolve;
          }),
      );
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.pendingRepairBindingId).toBe(active.id));

    const operation = api.repairBindingId(active.id);

    expect(api.store.status).toBe('working');
    expect(api.store.operation).toEqual({ kind: 'working', message: '正在恢复控件...' });

    resolveControls([{ InternalId: 'restored', Tag: active.controlTag }]);
    await operation;
  });

  it('点击恢复前重新枚举，Tag 已被其他会话恢复时只定位且不重复写入', async () => {
    const fixture = createFixture();
    const active = binding({ status: 'ACTIVE', version: 2 });
    vi.mocked(fixture.client.fetchAnnotationContext).mockResolvedValue(annotationContext({ bindings: [active] }));
    vi.mocked(fixture.bridge.getRuntimeOptions).mockReturnValue({ ...validOptions, repairBindingId: 'binding' });
    vi.mocked(fixture.bridge.listControls)
      .mockResolvedValueOnce([])
      .mockResolvedValueOnce([{ InternalId: 'restored-elsewhere', Tag: active.controlTag }]);
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.pendingRepairBindingId).toBe('binding'));

    await api.repairBindingId('binding');

    expect(fixture.bridge.selectControl).toHaveBeenCalledWith('restored-elsewhere', expect.any(AbortSignal));
    expect(fixture.bridge.insertBinding).not.toHaveBeenCalled();
    expect(fixture.client.confirmRepair).not.toHaveBeenCalled();
  });

  it('repair confirm 网络失败后发现 Tag 已存在时复用同一 operationCode 幂等确认', async () => {
    const fixture = createFixture();
    const active = binding({ status: 'ACTIVE', version: 2 });
    vi.mocked(fixture.client.fetchAnnotationContext).mockResolvedValue(annotationContext({ bindings: [active] }));
    vi.mocked(fixture.bridge.getRuntimeOptions).mockReturnValue({ ...validOptions, repairBindingId: 'binding' });
    vi.mocked(fixture.bridge.listControls)
      .mockResolvedValueOnce([])
      .mockResolvedValueOnce([])
      .mockResolvedValueOnce([{ BindingId: active.id, InternalId: 'restored', Tag: active.controlTag }])
      .mockResolvedValueOnce([{ BindingId: active.id, InternalId: 'restored', Tag: active.controlTag }]);
    vi.mocked(fixture.client.confirmRepair).mockRejectedValueOnce(new TypeError('network failed')).mockResolvedValueOnce(undefined);
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.pendingRepairBindingId).toBe('binding'));

    await expect(api.repairBindingId('binding')).rejects.toThrow('network failed');
    await expect(api.repairBindingId('binding')).resolves.toBeUndefined();

    expect(fixture.client.fetchRepairContext).toHaveBeenCalledOnce();
    expect(fixture.bridge.insertBinding).toHaveBeenCalledOnce();
    expect(fixture.client.confirmRepair).toHaveBeenCalledTimes(2);
    expect(vi.mocked(fixture.client.confirmRepair).mock.calls.map((call) => call[2].operationCode)).toEqual(['check-op', 'check-op']);
    expect(fixture.bridge.selectControl).toHaveBeenCalledWith('restored', expect.any(AbortSignal));
  });

  it('点击恢复前重新枚举，Tag 出现多次时拒绝写入和确认', async () => {
    const fixture = createFixture();
    const active = binding({ status: 'ACTIVE', version: 2 });
    vi.mocked(fixture.client.fetchAnnotationContext).mockResolvedValue(annotationContext({ bindings: [active] }));
    vi.mocked(fixture.bridge.getRuntimeOptions).mockReturnValue({ ...validOptions, repairBindingId: 'binding' });
    vi.mocked(fixture.bridge.listControls)
      .mockResolvedValueOnce([])
      .mockResolvedValueOnce([
        { InternalId: 'duplicate-1', Tag: active.controlTag },
        { InternalId: 'duplicate-2', Tag: active.controlTag },
      ]);
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.pendingRepairBindingId).toBe('binding'));

    await expect(api.repairBindingId('binding')).rejects.toThrow('修复目标 Tag 在文档中出现多次');

    expect(fixture.bridge.selectControl).not.toHaveBeenCalled();
    expect(fixture.bridge.insertBinding).not.toHaveBeenCalled();
    expect(fixture.client.confirmRepair).not.toHaveBeenCalled();
  });

  it('repairBindingId 启动时 Tag 已存在只定位，不加载修复上下文或重复插入', async () => {
    const fixture = createFixture();
    const active = binding({ status: 'ACTIVE', version: 2 });
    vi.mocked(fixture.client.fetchAnnotationContext).mockResolvedValue(annotationContext({ bindings: [active] }));
    vi.mocked(fixture.bridge.getRuntimeOptions).mockReturnValue({ ...validOptions, repairBindingId: 'binding' });
    vi.mocked(fixture.bridge.listControls).mockResolvedValue([{ InternalId: 'existing', Tag: active.controlTag }]);
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));

    expect(fixture.bridge.selectControl).toHaveBeenCalledWith('existing', expect.any(AbortSignal));
    expect(fixture.client.fetchRepairContext).not.toHaveBeenCalled();
    expect(fixture.bridge.insertBinding).not.toHaveBeenCalled();
    expect(api.pendingRepairBindingId).toBeNull();
  });

  it('repair 初始化定位响应在 dispose 后返回时不覆盖新会话 operation', async () => {
    const fixture = createFixture();
    const active = binding({ status: 'ACTIVE', version: 2 });
    let resolve!: () => void;
    vi.mocked(fixture.client.fetchAnnotationContext).mockResolvedValue(annotationContext({ bindings: [active] }));
    vi.mocked(fixture.bridge.getRuntimeOptions).mockReturnValue({ ...validOptions, repairBindingId: active.id });
    vi.mocked(fixture.bridge.listControls).mockResolvedValue([{ InternalId: 'existing', Tag: active.controlTag }]);
    vi.mocked(fixture.bridge.selectControl).mockReturnValue(
      new Promise((done) => {
        resolve = done;
      }),
    );
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(fixture.bridge.selectControl).toHaveBeenCalledOnce());
    api.dispose();

    const freshFixture = createFixture();
    const freshApi = useTemplateAnnotator(freshFixture.bridge, freshFixture.client);
    freshFixture.ready();
    await vi.waitFor(() => expect(freshApi.store.status).toBe('ready'));
    const operationAfterFreshReady = { ...freshApi.store.operation };
    resolve();
    await new Promise((done) => setTimeout(done, 0));
    expect(freshApi.store.operation).toEqual(operationAfterFreshReady);
  });

  it('repair 初始化上下文响应在 dispose 后返回时不回填 pending repair 或新会话 operation', async () => {
    const fixture = createFixture();
    const active = binding({ status: 'ACTIVE', version: 2 });
    let resolve!: (value: TemplateRepairContext) => void;
    vi.mocked(fixture.client.fetchAnnotationContext).mockResolvedValue(annotationContext({ bindings: [active] }));
    vi.mocked(fixture.bridge.getRuntimeOptions).mockReturnValue({ ...validOptions, repairBindingId: active.id });
    vi.mocked(fixture.bridge.listControls).mockResolvedValue([]);
    vi.mocked(fixture.client.fetchRepairContext).mockReturnValue(
      new Promise((done) => {
        resolve = done;
      }),
    );
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(fixture.client.fetchRepairContext).toHaveBeenCalledOnce());
    api.dispose();

    const freshFixture = createFixture();
    const freshApi = useTemplateAnnotator(freshFixture.bridge, freshFixture.client);
    freshFixture.ready();
    await vi.waitFor(() => expect(freshApi.store.status).toBe('ready'));
    const operationAfterFreshReady = { ...freshApi.store.operation };
    resolve({ ...check(), binding: active });
    await new Promise((done) => setTimeout(done, 0));
    expect(api.pendingRepairBindingId).toBeNull();
    expect(freshApi.store.operation).toEqual(operationAfterFreshReady);
  });

  it('字段升级按 check→文档改写→枚举→confirm 执行并携带目标版本', async () => {
    const fixture = createFixture();
    const active = binding({ status: 'ACTIVE', version: 2 });
    vi.mocked(fixture.client.fetchAnnotationContext).mockResolvedValue(annotationContext({ bindings: [active] }));
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));
    fixture.order.splice(0);

    await api.upgradeBinding('binding', 'published-v2');

    expect(fixture.client.checkBindingVersionUpgrade).toHaveBeenCalledWith(expect.anything(), 'binding', { targetFieldVersionId: 'published-v2' });
    expect(fixture.bridge.applyControlPlan).toHaveBeenCalledWith(expect.anything(), expect.anything(), expect.any(AbortSignal));
    expect(fixture.order).toEqual(['document', 'list', 'confirm-upgrade']);
    expect(fixture.client.confirmBindingVersionUpgrade).toHaveBeenCalledWith(
      expect.anything(),
      'binding',
      expect.objectContaining({ targetFieldVersionId: 'published-v2' }),
    );
  });

  it('局部变更计划不会把未涉及的其他 ACTIVE 绑定标记为 REMOVED', async () => {
    const fixture = createFixture();
    const active = binding({ status: 'ACTIVE', version: 2 });
    const unrelated = binding({ controlTag: 'eln.field.other', fieldCode: 'other', fieldId: 'other-field', id: 'other', status: 'ACTIVE', version: 3 });
    vi.mocked(fixture.client.fetchAnnotationContext).mockResolvedValue(annotationContext({ bindings: [active, unrelated] }));
    vi.mocked(fixture.client.checkBindingVersionUpgrade).mockResolvedValue(
      check({
        controls: [{ bindingId: active.id, controlTag: active.controlTag }],
      }),
    );
    vi.mocked(fixture.bridge.listControls)
      .mockResolvedValueOnce([])
      .mockResolvedValueOnce([{ BindingId: active.id, Tag: active.controlTag }]);
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));

    await api.upgradeBinding(active.id, 'published-v2');

    expect(api.bindings.find((item) => item.id === unrelated.id)?.status).toBe('ACTIVE');
  });

  it('删除必须 check→文档改写→枚举→confirm，confirm 失败时绑定保持 ACTIVE', async () => {
    const fixture = createFixture();
    const active = binding({ status: 'ACTIVE', version: 2 });
    vi.mocked(fixture.client.fetchAnnotationContext).mockResolvedValue(annotationContext({ bindings: [active] }));
    const enumeratedControls = [{ BindingId: 'unrelated', Tag: 'eln.field.unrelated' }, { Tag: '' }, { Tag: '   ' }];
    vi.mocked(fixture.bridge.listControls).mockImplementation(async () => {
      fixture.order.push('list');
      return enumeratedControls;
    });
    vi.mocked(fixture.client.confirmBindingRemoval).mockRejectedValue(new Error('确认冲突'));
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));
    fixture.order.splice(0);
    await expect(api.removeBinding('binding')).rejects.toThrow('确认冲突');
    expect(fixture.order).toEqual(['document', 'list']);
    expect(fixture.client.confirmBindingRemoval).toHaveBeenCalledOnce();
    expect(fixture.client.confirmBindingRemoval).toHaveBeenCalledWith(
      expect.anything(),
      active.id,
      expect.objectContaining({ observedControlTags: ['eln.field.unrelated'] }),
    );
    expect(api.bindings[0]?.status).toBe('ACTIVE');
    expect(api.store.operation).toEqual({ kind: 'error', message: '删除失败：确认冲突' });
  });

  it('删除孤立控件只操作 DOCX 控件并更新字段列表', async () => {
    const fixture = createFixture();
    const remainingControls = [{ InternalId: 'remaining-id', Tag: 'eln.field.other' }];
    vi.mocked(fixture.bridge.deleteControl).mockResolvedValue(remainingControls);
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));
    fixture.order.splice(0);

    await api.removeOrphanControl('orphan-id');

    expect(fixture.bridge.disableTrackRevisions).toHaveBeenCalledOnce();
    expect(fixture.bridge.deleteControl).toHaveBeenCalledWith('orphan-id');
    expect(fixture.client.checkBindingRemoval).not.toHaveBeenCalled();
    expect(fixture.client.confirmBindingRemoval).not.toHaveBeenCalled();
    expect(api.store.controls).toEqual(remainingControls);
    expect(api.store.operation).toEqual({ kind: 'success', message: '异常控件已删除' });
  });

  it('引用提升必须 check→文档改写→枚举→confirm', async () => {
    const fixture = createFixture();
    const reference = binding({ controlRole: 'REFERENCE', controlTag: 'eln.ref.result.abc', id: 'reference', primaryBindingId: 'binding', status: 'ACTIVE' });
    vi.mocked(fixture.client.fetchAnnotationContext).mockResolvedValue(annotationContext({ bindings: [binding({ status: 'ACTIVE' }), reference] }));
    vi.mocked(fixture.client.checkReferencePromotion).mockResolvedValue(
      check({
        controls: [{ bindingId: reference.id, controlRole: 'PRIMARY', controlTag: 'eln.field.result', primaryBindingId: null }],
      }),
    );
    vi.mocked(fixture.bridge.listControls).mockImplementation(async () => {
      fixture.order.push('list');
      return [{ BindingId: reference.id, Tag: 'eln.field.result' }, { BindingId: 'unrelated', Tag: 'eln.field.unrelated' }, { Tag: '' }, { Tag: '   ' }];
    });
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));
    fixture.order.splice(0);
    await api.promoteReference('reference');
    expect(fixture.order).toEqual(['document', 'list', 'confirm-promote']);
    expect(fixture.client.confirmReferencePromotion).toHaveBeenCalledWith(
      expect.anything(),
      reference.id,
      expect.objectContaining({ observedControlTags: ['eln.field.result', 'eln.field.unrelated'] }),
    );
  });

  it('引用提升 confirm 网络失败后用同一 operationCode 重新枚举并幂等重试，不回滚 DOCX', async () => {
    const fixture = createFixture();
    const reference = binding({ controlRole: 'REFERENCE', controlTag: 'eln.ref.result.abc', id: 'reference', primaryBindingId: 'binding', status: 'ACTIVE' });
    const promotionCheck = check({
      controls: [
        {
          bindingId: reference.id,
          controlAlias: '结果',
          controlConfig: { placeholder: '请输入' },
          controlRole: 'PRIMARY',
          controlTag: 'eln.field.result',
          controlType: 'TEXT',
          fieldVersionId: 'version',
          primaryBindingId: null,
        },
      ],
      operationCode: 'promote-op',
    });
    vi.mocked(fixture.client.fetchAnnotationContext).mockResolvedValue(annotationContext({ bindings: [binding({ status: 'ACTIVE' }), reference] }));
    vi.mocked(fixture.client.checkReferencePromotion).mockResolvedValue(promotionCheck);
    vi.mocked(fixture.bridge.listControls).mockResolvedValue([{ BindingId: reference.id, Tag: 'eln.field.result' }]);
    vi.mocked(fixture.client.confirmReferencePromotion).mockRejectedValueOnce(new TypeError('network failed')).mockResolvedValueOnce(undefined);
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));

    await expect(api.promoteReference(reference.id)).rejects.toThrow('network failed');
    await expect(api.promoteReference(reference.id)).resolves.toBeUndefined();

    expect(fixture.bridge.applyControlPlan).toHaveBeenCalledTimes(2);
    expect(fixture.bridge.listControls).toHaveBeenCalledTimes(3);
    expect(fixture.client.checkReferencePromotion).toHaveBeenCalledOnce();
    expect(fixture.client.confirmReferencePromotion).toHaveBeenCalledTimes(2);
    expect(vi.mocked(fixture.client.confirmReferencePromotion).mock.calls.map((call) => call[2].operationCode)).toEqual(['promote-op', 'promote-op']);
    expect(fixture.bridge.deleteControl).not.toHaveBeenCalled();
  });

  it.each([
    ['删除', 'confirmBindingRemoval', (api: ReturnType<typeof useTemplateAnnotator>) => api.removeBinding('binding')],
    ['引用提升', 'confirmReferencePromotion', (api: ReturnType<typeof useTemplateAnnotator>) => api.promoteReference('binding')],
    ['字段升级', 'confirmBindingVersionUpgrade', (api: ReturnType<typeof useTemplateAnnotator>) => api.upgradeBinding('binding', 'published-v2')],
  ] as const)('%s确认响应在 dispose 后才返回时不回填本地状态', async (_label, confirmMethod, execute) => {
    const fixture = createFixture();
    const active = binding({ status: 'ACTIVE', version: 2 });
    let resolve!: () => void;
    vi.mocked(fixture.client.fetchAnnotationContext).mockResolvedValue(annotationContext({ bindings: [active] }));
    vi.mocked(fixture.bridge.listControls)
      .mockResolvedValueOnce([])
      .mockResolvedValueOnce([{ BindingId: active.id, Tag: active.controlTag }]);
    vi.mocked(fixture.client[confirmMethod]).mockReturnValue(
      new Promise((done) => {
        resolve = done;
      }),
    );
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));

    const operation = execute(api);
    await vi.waitFor(() => expect(fixture.client[confirmMethod]).toHaveBeenCalledOnce());
    api.dispose();
    const controlsAfterDispose = [...api.store.controls];
    resolve();
    await operation;

    expect(api.bindings).toEqual([]);
    expect(api.contextStore.context).toBeNull();
    expect(api.store.controls).toEqual(controlsAfterDispose);
  });

  it('dispose 后晚到的 confirm 401 不清空新会话上下文', async () => {
    const fixture = createFixture();
    const active = binding({ status: 'ACTIVE', version: 2 });
    let reject!: (error: Error) => void;
    vi.mocked(fixture.client.fetchAnnotationContext).mockResolvedValue(annotationContext({ bindings: [active] }));
    vi.mocked(fixture.bridge.listControls)
      .mockResolvedValueOnce([])
      .mockResolvedValueOnce([{ BindingId: active.id, Tag: active.controlTag }]);
    vi.mocked(fixture.client.confirmBindingRemoval).mockReturnValue(
      new Promise((_, fail) => {
        reject = fail;
      }),
    );
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));

    const operation = api.removeBinding(active.id);
    await vi.waitFor(() => expect(fixture.client.confirmBindingRemoval).toHaveBeenCalledOnce());
    api.dispose();

    const freshFixture = createFixture();
    const freshApi = useTemplateAnnotator(freshFixture.bridge, freshFixture.client);
    freshFixture.ready();
    await vi.waitFor(() => expect(freshApi.store.status).toBe('ready'));
    reject(new TemplateAnnotationApiError(401, '旧会话 Ticket 已过期'));
    await expect(operation).rejects.toThrow('旧会话 Ticket 已过期');

    expect(freshApi.contextStore.context).not.toBeNull();
    expect(freshApi.store.status).toBe('ready');
  });

  it('401/403 Ticket 失败后清空上下文并停止后续写操作', async () => {
    const fixture = createFixture();
    vi.mocked(fixture.client.reservePrimaryBinding).mockRejectedValue(new TemplateAnnotationApiError(401, 'Ticket 已过期'));
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));
    await expect(api.insertSelectedField(option)).rejects.toThrow('Ticket 已过期');
    expect(api.contextStore.context).toBeNull();
    await expect(api.insertSelectedField(option)).rejects.toThrow('插件运行上下文不可用');
    expect(fixture.client.reservePrimaryBinding).toHaveBeenCalledTimes(1);
  });

  it('activate 返回 Ticket 过期后不再发送 cancel 补偿写请求', async () => {
    const fixture = createFixture();
    vi.mocked(fixture.bridge.listControls)
      .mockResolvedValueOnce([])
      .mockResolvedValueOnce([])
      .mockResolvedValueOnce([{ Tag: 'eln.field.result' }]);
    vi.mocked(fixture.client.activateBinding).mockRejectedValue(new TemplateAnnotationApiError(401, 'Ticket 已过期'));
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    await vi.waitFor(() => expect(api.store.status).toBe('ready'));
    await expect(api.insertSelectedField(option)).rejects.toThrow('Ticket 已过期');
    expect(fixture.client.cancelBinding).not.toHaveBeenCalled();
  });

  it('dispose 后晚到 context 响应不覆盖新状态', async () => {
    const fixture = createFixture();
    let resolve!: (value: TemplateAnnotationContext) => void;
    vi.mocked(fixture.client.fetchAnnotationContext).mockReturnValue(
      new Promise((done) => {
        resolve = done;
      }),
    );
    const api = useTemplateAnnotator(fixture.bridge, fixture.client);
    fixture.ready();
    api.dispose();
    resolve(annotationContext({ bindings: [binding({ status: 'ACTIVE' })] }));
    await Promise.resolve();
    expect(api.bindings).toEqual([]);
    expect(api.contextStore.context).toBeNull();
  });
});
