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

import {
  activateBinding,
  activateFormulaBinding,
  cancelBinding,
  cancelFormulaBinding,
  changeCheckboxGroup,
  changeCheckboxGroupRule,
  checkBindingRemoval,
  checkBindingVersionUpgrade,
  checkReferencePromotion,
  confirmBindingRemoval,
  confirmBindingVersionUpgrade,
  confirmReferencePromotion,
  confirmRepair,
  confirmVersionCopy,
  confirmFormulaBindingRemoval,
  confirmFormulaBindingUpgrade,
  confirmFormulaVersionCopy,
  fetchCrossTemplateScalarCandidates,
  fetchCrossTemplateSourceTemplates,
  fetchFormulaMappings,
  fetchAnnotationContext,
  fetchRepairContext,
  reservePrimaryBinding,
  reserveCrossTemplateScalarBinding,
  reservePrivateField,
  reserveReferenceBinding,
  reserveFormulaBinding,
  suggestCode,
  TemplateAnnotationApiError,
} from '../../src/api/template-annotation';
import { toTemplateFieldOptions, type TemplateAnnotationContext } from '../../src/domain/types';

const client = { apiBaseUrl: 'https://api.example.com/', ticket: 'opaque' };

function actionResult(data: unknown, init?: ResponseInit): Response {
  return new Response(JSON.stringify({ code: init?.status ?? 200, msg: 'success', data }), {
    headers: { 'Content-Type': 'application/json' },
    ...init,
  });
}

function successfulFetch(data: unknown = null) {
  const fetchMock = vi.fn().mockImplementation(() => Promise.resolve(actionResult(data)));
  vi.stubGlobal('fetch', fetchMock);
  return fetchMock;
}

afterEach(() => {
  vi.unstubAllGlobals();
});

describe('template annotation Ticket API', () => {
  it('requests a code suggestion with the Ticket header and returns the generated code', async () => {
    const fetchMock = successfulFetch({ code: 'jianyan_jieguo' });

    await expect(suggestCode(client, { text: '检验结果' })).resolves.toBe('jianyan_jieguo');

    expect(fetchMock).toHaveBeenCalledWith(
      'https://api.example.com/api/eln/onlyoffice/templateAnnotation/codeSuggestions',
      expect.objectContaining({
        body: JSON.stringify({ text: '检验结果' }),
        headers: { Authorization: 'Ticket opaque', 'Content-Type': 'application/json' },
        method: 'POST',
      }),
    );
  });

  it('rejects an invalid code suggestion response', async () => {
    successfulFetch({ code: '' });

    await expect(suggestCode(client, { text: '检验结果' })).rejects.toMatchObject({
      message: '编码生成接口响应无效',
      status: 200,
    });
  });

  it('loads context with the Ticket header and never places the ticket in the URL', async () => {
    const context = { templateId: 'template-1', templateCode: 'TPL-001', fields: [], bindings: [] };
    const fetchMock = successfulFetch(context);

    await expect(fetchAnnotationContext(client)).resolves.toEqual(context);

    expect(fetchMock).toHaveBeenCalledWith(
      'https://api.example.com/api/eln/onlyoffice/templateAnnotation/context',
      expect.objectContaining({ method: 'GET', headers: { Authorization: 'Ticket opaque' } }),
    );
    expect(fetchMock.mock.calls[0]?.[0]).not.toContain('opaque');
  });

  it.each([
    [401, '标注 Ticket 已过期'],
    [403, '无模板标注权限'],
    [409, '模板状态已变化'],
    [422, '字段版本无效'],
  ])('maps HTTP %i and the backend message to TemplateAnnotationApiError', async (status, msg) => {
    vi.stubGlobal(
      'fetch',
      vi.fn().mockResolvedValue(
        new Response(JSON.stringify({ code: status, msg, data: null }), {
          status,
          statusText: 'Failure',
          headers: { 'Content-Type': 'application/json' },
        }),
      ),
    );

    const error = await fetchAnnotationContext(client).catch((reason: unknown) => reason);

    expect(error).toBeInstanceOf(TemplateAnnotationApiError);
    expect(error).toMatchObject({ status, message: msg });
  });

  it('passes one AbortSignal through the shared request client', async () => {
    const fetchMock = successfulFetch({ templateId: 'template-1', templateCode: 'TPL-001', fields: [], bindings: [] });
    const controller = new AbortController();

    await fetchAnnotationContext({ ...client, signal: controller.signal });

    expect(fetchMock).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({ signal: controller.signal }));
  });

  it('读取跨模板文本候选并以来源模板和字段版本预留普通主控件', async () => {
    const fetchMock = successfulFetch({ id: 'cross-binding' });
    const reserve = {
      operationCode: 'cross-reserve',
      repeatRequest: false as const,
      sourceFieldId: 'source-field',
      sourceFieldVersionId: 'source-version',
      sourceTemplateCode: 'SOURCE-TPL',
      sourceTemplateId: 'source-template',
      stepId: 'step-1',
    };

    await fetchCrossTemplateSourceTemplates(client);
    await fetchCrossTemplateScalarCandidates(client, 'source/template');
    await reserveCrossTemplateScalarBinding(client, reserve);

    expect(fetchMock.mock.calls.map(([url, init]) => [url, init?.method, init?.body])).toEqual([
      ['https://api.example.com/api/eln/onlyoffice/templateAnnotation/crossTemplateSourceTemplates', 'GET', undefined],
      ['https://api.example.com/api/eln/onlyoffice/templateAnnotation/crossTemplateTextCandidates?sourceTemplateId=source%2Ftemplate', 'GET', undefined],
      [
        'https://api.example.com/api/eln/onlyoffice/templateAnnotation/crossTemplateTextBindings/reserve',
        'POST',
        JSON.stringify(reserve),
      ],
    ]);
    for (const [, init] of fetchMock.mock.calls) {
      expect(init?.headers).toMatchObject({ Authorization: 'Ticket opaque' });
    }
  });

  it('accepts a successful void ActionResult when Jackson omits null data', async () => {
    vi.stubGlobal(
      'fetch',
      vi.fn().mockResolvedValue(
        new Response(JSON.stringify({ code: 200, msg: 'success' }), {
          status: 200,
          headers: { 'Content-Type': 'application/json' },
        }),
      ),
    );

    await expect(activateBinding(client, 'binding-1', { operationCode: 'op-activate' })).resolves.toBeUndefined();
  });

  it('flattens backend field identities and versions into complete immutable selection options', () => {
    const context: TemplateAnnotationContext = {
      templateId: 'template-1',
      templateCode: 'TPL-001',
      bindings: [],
      stepConfiguration: {
        singleStep: true,
        steps: [{ id: 'step-1', name: '实验步骤', order: 1 }],
      },
      fields: [
        {
          id: 'field-1',
          ziduanBianma: 'jianyan_yiju',
          zuoyongFanwei: 'GLOBAL',
          mubanBianhao: null,
          dangqianZiduanBanbenId: 'version-current',
          bizEnabled: 'enabled',
          beizhu: null,
          versions: [
            {
              id: 'version-current',
              ziduanDingyiId: 'field-1',
              banbenHao: 2,
              ziduanMingcheng: '检验依据',
              shujuLeixing: 'TEXT',
              danwei: null,
              shifouBitian: 1,
              piciMoshi: 'RECORD_PRIVATE',
              binding: { read: { source: 'LIMS_TASK', property: 'test.basis' }, write: { target: 'ELN_RECORD' } },
              kongjianPeizhi: { placeholder: '请输入检验依据' },
              recommendedCheckboxGroupCode: 'inspection_result',
              recommendedCheckboxMinSelected: 1,
              recommendedCheckboxMaxSelected: 2,
              bizStatus: 'PUBLISHED',
              zuofeiYuanyin: null,
              chuangjianShijian: null,
              gengxinShijian: null,
            },
          ],
        },
      ],
    };

    expect(toTemplateFieldOptions(context.fields)).toEqual([
      {
        alias: '检验依据',
        batchMode: 'RECORD_PRIVATE',
        controlConfig: { placeholder: '请输入检验依据' },
        fieldId: 'field-1',
        fieldCode: 'jianyan_yiju',
        fieldVersionId: 'version-current',
        scope: 'GLOBAL',
        label: '检验依据',
        required: true,
        recommendedCheckboxGroupCode: 'inspection_result',
        recommendedCheckboxMinSelected: 1,
        recommendedCheckboxMaxSelected: 2,
        type: 'TEXT',
        source: 'LIMS_TASK',
        versionStatus: 'PUBLISHED',
        writeTarget: 'ELN_RECORD',
        isCurrent: true,
      },
    ]);
  });

  it('covers every Task 9 endpoint and sends authoritative IDs instead of fieldCode', async () => {
    const fetchMock = successfulFetch({ id: 'binding-1' });
    const snapshots = [{ bindingId: 'binding-1', controlTag: 'eln.field.server_code' }];
    const confirmation = { operationCode: 'op-confirm', expectedVersions: { 'binding-1': 1 }, controls: snapshots, observedControlTags: ['eln.field.server_code'] };

    await reservePrivateField(client, {
      checkboxGroupCode: 'inspection_result',
      operationCode: 'op-private',
      ziduanMingcheng: '观察结果',
      shujuLeixing: 'CHECKBOX',
      stepId: 'step-1',
    });
    await reservePrimaryBinding(client, {
      checkboxGroupCode: 'inspection_result',
      fieldId: 'field-1',
      fieldVersionId: 'version-1',
      operationCode: 'op-primary',
      repeatRequest: false,
      stepId: 'step-1',
    });
    await changeCheckboxGroup(client, 'binding/1', {
      checkboxGroupCode: null,
      expectedVersion: 2,
      operationCode: 'op-checkbox-group',
    });
    await changeCheckboxGroupRule(client, 'inspection/result', {
      expectedVersion: 3,
      maxSelected: 4,
      minSelected: 1,
      operationCode: 'op-checkbox-group-rule',
    });
    await reserveReferenceBinding(client, 'binding/1', { operationCode: 'op-reference' });
    await activateBinding(client, 'binding-1', { operationCode: 'op-activate' });
    await cancelBinding(client, 'binding-1', { operationCode: 'op-cancel', reason: '插入失败' });
    await fetchRepairContext(client, 'binding-1');
    await confirmRepair(client, 'binding-1', confirmation);
    await checkBindingVersionUpgrade(client, 'binding-1', { targetFieldVersionId: 'version-2' });
    await confirmBindingVersionUpgrade(client, 'binding-1', { ...confirmation, targetFieldVersionId: 'version-2' });
    await checkBindingRemoval(client, 'binding-1');
    await confirmBindingRemoval(client, 'binding-1', confirmation);
    await checkReferencePromotion(client, 'binding-1');
    await confirmReferencePromotion(client, 'binding-1', confirmation);
    await confirmVersionCopy(client, { upgradeOperationCode: 'op-copy', expectedVersions: { 'binding-1': 1 }, controls: snapshots });

    const calls = fetchMock.mock.calls.map(([url, init]) => [url, init?.method, init?.body]);
    expect(calls).toEqual([
      [
        'https://api.example.com/api/eln/onlyoffice/templateAnnotation/privateFields/reserve',
        'POST',
        JSON.stringify({
          checkboxGroupCode: 'inspection_result',
          operationCode: 'op-private',
          ziduanMingcheng: '观察结果',
          shujuLeixing: 'CHECKBOX',
          stepId: 'step-1',
        }),
      ],
      [
        'https://api.example.com/api/eln/onlyoffice/templateAnnotation/bindings/reserve',
        'POST',
        JSON.stringify({
          checkboxGroupCode: 'inspection_result',
          fieldId: 'field-1',
          fieldVersionId: 'version-1',
          operationCode: 'op-primary',
          repeatRequest: false,
          stepId: 'step-1',
        }),
      ],
      [
        'https://api.example.com/api/eln/onlyoffice/templateAnnotation/bindings/binding%2F1/checkboxGroup',
        'POST',
        JSON.stringify({ checkboxGroupCode: null, expectedVersion: 2, operationCode: 'op-checkbox-group' }),
      ],
      [
        'https://api.example.com/api/eln/onlyoffice/templateAnnotation/checkboxGroups/inspection%2Fresult',
        'POST',
        JSON.stringify({ expectedVersion: 3, maxSelected: 4, minSelected: 1, operationCode: 'op-checkbox-group-rule' }),
      ],
      [
        'https://api.example.com/api/eln/onlyoffice/templateAnnotation/bindings/binding%2F1/references/reserve',
        'POST',
        JSON.stringify({ operationCode: 'op-reference' }),
      ],
      ['https://api.example.com/api/eln/onlyoffice/templateAnnotation/bindings/binding-1/activate', 'POST', JSON.stringify({ operationCode: 'op-activate' })],
      [
        'https://api.example.com/api/eln/onlyoffice/templateAnnotation/bindings/binding-1/cancel',
        'POST',
        JSON.stringify({ operationCode: 'op-cancel', reason: '插入失败' }),
      ],
      ['https://api.example.com/api/eln/onlyoffice/templateAnnotation/bindings/binding-1/repairContext', 'GET', undefined],
      ['https://api.example.com/api/eln/onlyoffice/templateAnnotation/bindings/binding-1/repairConfirm', 'POST', JSON.stringify(confirmation)],
      [
        'https://api.example.com/api/eln/onlyoffice/templateAnnotation/bindings/binding-1/versionUpgradeCheck',
        'POST',
        JSON.stringify({ targetFieldVersionId: 'version-2' }),
      ],
      [
        'https://api.example.com/api/eln/onlyoffice/templateAnnotation/bindings/binding-1/versionUpgradeConfirm',
        'POST',
        JSON.stringify({ ...confirmation, targetFieldVersionId: 'version-2' }),
      ],
      ['https://api.example.com/api/eln/onlyoffice/templateAnnotation/bindings/binding-1/removeCheck', 'POST', undefined],
      ['https://api.example.com/api/eln/onlyoffice/templateAnnotation/bindings/binding-1/removeConfirm', 'POST', JSON.stringify(confirmation)],
      ['https://api.example.com/api/eln/onlyoffice/templateAnnotation/bindings/binding-1/promoteCheck', 'POST', undefined],
      ['https://api.example.com/api/eln/onlyoffice/templateAnnotation/bindings/binding-1/promoteConfirm', 'POST', JSON.stringify(confirmation)],
      [
        'https://api.example.com/api/eln/onlyoffice/templateAnnotation/versionCopy/confirm',
        'POST',
        JSON.stringify({ upgradeOperationCode: 'op-copy', expectedVersions: { 'binding-1': 1 }, controls: snapshots }),
      ],
    ]);
    expect(JSON.parse(fetchMock.mock.calls[1]?.[1]?.body as string)).not.toHaveProperty('fieldCode');
    for (const [, init] of fetchMock.mock.calls) {
      expect(init?.headers).toMatchObject({ Authorization: 'Ticket opaque' });
    }
  });

  it('从独立公式上下文端点读取候选和映射，不占用兼容列表端点', async () => {
    const fetchMock = successfulFetch({ id: 'formula-binding' });
    const request = {
      formulaVersionId: 'formula-version', parameterBindings: { amount: 'input-binding' }, resultBindingId: 'result-binding', operationCode: 'formula-op',
      jieguoSuoKuaizhao: [{ bindingId: 'result-binding', controlRole: 'PRIMARY', controlTag: 'eln.field.result', lock: 'unlocked' }],
    };
    const controls = [{ bindingId: 'result-binding', controlRole: 'PRIMARY', controlTag: 'eln.field.result', lock: 'sdtContentLocked' }];

    await fetchFormulaMappings(client);
    await reserveFormulaBinding(client, request);
    await activateFormulaBinding(client, 'formula-binding', { controls, expectedVersion: 1, operationCode: 'formula-op' });
    await cancelFormulaBinding(client, 'formula-binding', { expectedVersion: 1, operationCode: 'formula-op', reason: 'failed' });
    await confirmFormulaBindingUpgrade(client, 'formula-binding', { controls, expectedVersion: 1, operationCode: 'upgrade-op', target: request });
    await confirmFormulaBindingRemoval(client, 'formula-binding', { controls: [{ ...controls[0], lock: '' }], expectedVersion: 1, operationCode: 'remove-op' });
    await confirmFormulaVersionCopy(client, { controls, expectedVersions: { 'formula-binding': 1 }, upgradeOperationCode: 'copy-op' });

    expect(fetchMock.mock.calls.map(([url]) => url)).toEqual([
      'https://api.example.com/api/eln/onlyoffice/templateAnnotation/formulaContext',
      'https://api.example.com/api/eln/onlyoffice/templateAnnotation/formulaBindings/reserve',
      'https://api.example.com/api/eln/onlyoffice/templateAnnotation/formulaBindings/formula-binding/activate',
      'https://api.example.com/api/eln/onlyoffice/templateAnnotation/formulaBindings/formula-binding/cancel',
      'https://api.example.com/api/eln/onlyoffice/templateAnnotation/formulaBindings/formula-binding/upgradeConfirm',
      'https://api.example.com/api/eln/onlyoffice/templateAnnotation/formulaBindings/formula-binding/removeConfirm',
      'https://api.example.com/api/eln/onlyoffice/templateAnnotation/formulaBindings/versionCopy/confirm',
    ]);
    const reservePayload = JSON.parse(fetchMock.mock.calls[1]?.[1]?.body as string);
    expect(reservePayload).toEqual(request);
    expect(reservePayload).not.toHaveProperty('expression');
    expect(reservePayload).not.toHaveProperty('formulaCode');
    expect(reservePayload).not.toHaveProperty('result');
  });
});
