import type { FormulaFunction, ParameterSlot } from '#/api/x/eln/formulas';

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

import { CompletionContext } from '@codemirror/autocomplete';
import { EditorState } from '@codemirror/state';
import { EditorView } from '@codemirror/view';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import FormulaEditor from '../FormulaEditor.vue';
import editorSource from '../FormulaEditor.vue?raw';
import { createFormulaCompletions, formulaCompletionSource, formulaLanguage, insertFormulaText } from '../formulaLanguage';
import languageSource from '../formulaLanguage.ts?raw';
import { toEditorDiagnostics } from '../formulaRules';

const apiMocks = vi.hoisted(() => ({ validateFormula: vi.fn() }));

vi.mock('#/api/x/eln/formulas', async (importOriginal) => ({
  ...(await importOriginal<typeof import('#/api/x/eln/formulas')>()),
  validateFormula: apiMocks.validateFormula,
}));

const parameters: ParameterSlot[] = [{ canshuBianma: 'sample_weight', canshuMingcheng: '样品质量', paixu: 1, shifouBitian: 1, shujuLeixing: 'DECIMAL' }];
const functions: FormulaFunction[] = [
  {
    category: '数学',
    chineseName: '平均值',
    code: 'MEAN',
    description: '计算平均值',
    example: 'MEAN(replicates)',
    returnType: 'DECIMAL',
    signature: 'MEAN(DECIMAL_LIST)',
  },
];

describe('eLN formula editor', () => {
  beforeEach(() => {
    vi.stubGlobal(
      'ResizeObserver',
      class {
        disconnect() {}
        observe() {}
        unobserve() {}
      },
    );
    vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) => window.setTimeout(() => callback(Date.now()), 0));
    vi.stubGlobal('cancelAnimationFrame', (handle: number) => window.clearTimeout(handle));
  });

  afterEach(() => {
    vi.useRealTimers();
    vi.unstubAllGlobals();
    vi.restoreAllMocks();
    vi.clearAllMocks();
  });

  it('offers parameter codes with their business names and functions with signatures and examples', () => {
    const completions = createFormulaCompletions(parameters, functions);

    expect(completions).toContainEqual(
      expect.objectContaining({ apply: 'sample_weight', displayLabel: '样品质量', label: expect.stringContaining('sample_weight'), type: 'variable' }),
    );
    expect(completions).toContainEqual(
      expect.objectContaining({
        apply: 'MEAN(',
        displayLabel: '平均值',
        detail: 'MEAN(DECIMAL_LIST)',
        label: expect.stringContaining('MEAN'),
        type: 'function',
      }),
    );
  });

  it('filters resource completions and groups them by business category', () => {
    const parameterCompletions = createFormulaCompletions(parameters, functions, 'parameter');
    expect(parameterCompletions).toHaveLength(1);
    expect(parameterCompletions[0]).toMatchObject({
      apply: 'sample_weight',
      displayLabel: '样品质量',
      section: expect.objectContaining({ name: '参数' }),
      type: 'variable',
    });
    expect(parameterCompletions[0]?.label).toContain('样品质量');
    expect(parameterCompletions[0]?.label).toContain('sample_weight');

    const functionCompletions = createFormulaCompletions(parameters, functions, 'function');
    expect(functionCompletions).toHaveLength(1);
    expect(functionCompletions[0]).toMatchObject({
      apply: 'MEAN(',
      displayLabel: '平均值',
      section: expect.objectContaining({ name: '函数' }),
      type: 'function',
    });
  });

  it('replaces a completion prefix that starts at offset zero', () => {
    const source = formulaCompletionSource(parameters, functions);
    const result = source({ explicit: true, matchBefore: () => ({ from: 0, text: 'sam', to: 3 }), pos: 3 } as any);

    expect(result).toEqual(expect.objectContaining({ from: 0 }));
  });

  it('keeps completion candidates open while searching by Chinese name', () => {
    const source = formulaCompletionSource(parameters, functions);
    const state = EditorState.create({ doc: '样品' });
    const result = source(new CompletionContext(state, state.doc.length, false));

    expect(result).toEqual(expect.objectContaining({ from: 0 }));
  });

  it('inserts selected parameters and functions at the current selection', () => {
    expect(insertFormulaText('a + b', 4, 5, 'MEAN(')).toBe('a + MEAN(');
    expect(insertFormulaText('', 0, 0, 'sample_weight')).toBe('sample_weight');
  });

  it('maps backend UTF-16 positions and clears latex for syntax errors', () => {
    expect(toEditorDiagnostics([{ code: 'SYNTAX', end: 8, severity: 'ERROR', start: 4 }], 6)).toEqual([expect.objectContaining({ from: 4, to: 6 })]);
    expect(toEditorDiagnostics([{ code: 'SYNTAX', end: 5, severity: 'ERROR', start: 3 }], '样品😀+a'.length)).toEqual([
      expect.objectContaining({ from: 3, to: 5 }),
    ]);
    expect(editorSource).toContain('hasSyntaxError');
    expect(editorSource).toContain('latex: hasSyntaxError');
  });

  it('uses the requested CodeMirror extensions and destroys its EditorView', () => {
    expect(FormulaEditor).toBeTruthy();
    expect(editorSource).toContain('history()');
    expect(editorSource).toContain('historyKeymap');
    expect(editorSource).toContain('bracketMatching()');
    expect(editorSource).toContain('autocompletion');
    expect(editorSource).toContain('lineWrapping');
    expect(languageSource).toContain('StreamLanguage');
    expect(editorSource).toContain('editorView?.destroy()');
    expect(editorSource).toContain('min-height: 180px');
    expect(editorSource).toContain('max-height: 360px');
  });

  it('restores the editor top border when the readonly toolbar is hidden', async () => {
    const wrapper = mount(FormulaEditor, {
      props: { functions, modelValue: 'sample_weight', parameters, readonly: true, rule: { biaodashi: 'sample_weight', jieguoLeixing: 'DECIMAL', parameters } },
      global: { stubs: { 'a-alert': true, 'a-button': true } },
    });
    await nextTick();

    expect(wrapper.find('.formula-editor__toolbar').exists()).toBe(false);
    expect(wrapper.classes()).toContain('is-readonly');
    expect(editorSource).toMatch(/\.formula-editor\.is-readonly\s+\.formula-editor__surface\s*\{[^}]*border-top:\s*1px solid var\(--border-color-base\);/);
    wrapper.unmount();
  });

  it('tokenizes strings and logical, comparison, negation, and modulo operators', () => {
    const expression = "name == '样品' && !passed % 2";
    const stringTokens: string[] = [];
    const operatorTokens: string[] = [];
    formulaLanguage.parser.parse(expression).iterate({
      enter(node) {
        if (node.name === 'string') stringTokens.push(expression.slice(node.from, node.to));
        if (node.name === 'operator') operatorTokens.push(expression.slice(node.from, node.to));
      },
    });

    expect(stringTokens).toEqual(["'样品'"]);
    expect(operatorTokens).toEqual(['==', '&&', '!', '%']);
  });

  it('inserts helpers, validates immediately, displays backend diagnostics, and destroys the mounted EditorView', async () => {
    const destroy = vi.spyOn(EditorView.prototype, 'destroy');
    apiMocks.validateFormula.mockResolvedValue({ data: { diagnostics: [{ end: 2, message: '参数无效', severity: 'ERROR', start: 0 }], latex: 'a' } });
    const Button = defineComponent({ emits: ['click'], template: '<button @click="$emit(\'click\')"><slot /></button>' });
    const wrapper = mount(FormulaEditor, {
      attachTo: document.body,
      props: { functions, modelValue: '', parameters, rule: { biaodashi: '', jieguoLeixing: 'DECIMAL', parameters } },
      global: { stubs: { 'a-alert': true, 'a-button': Button } },
    });
    await nextTick();

    expect(wrapper.find('.cm-placeholder').text()).toBe('请输入公式表达式');
    expect(wrapper.findAll('button').map((button) => button.text())).toEqual(expect.arrayContaining(['插入参数', '插入函数']));
    await wrapper
      .findAll('button')
      .find((button) => button.text() === '插入参数')!
      .trigger('click');
    await nextTick();
    await vi.waitFor(() => expect(document.body.querySelector('.cm-tooltip-autocomplete')).not.toBeNull());
    expect(document.body.querySelector('.cm-tooltip-autocomplete')!.textContent).toContain('样品质量');
    expect(document.body.querySelector('.cm-tooltip-autocomplete')!.textContent).not.toContain('平均值');
    (wrapper.vm as any).insert('sample_weight');
    await (wrapper.vm as any).validate();
    await flushPromises();

    expect(apiMocks.validateFormula).toHaveBeenCalledWith(expect.objectContaining({ biaodashi: 'sample_weight' }), expect.any(AbortSignal));
    expect(wrapper.emitted('validation')?.at(-1)?.[0]).toEqual(expect.objectContaining({ latex: null }));
    expect(wrapper.find('.cm-lintRange').exists()).toBe(true);
    wrapper.unmount();
    expect(destroy).toHaveBeenCalledTimes(1);
  });

  it('clears old validation and revalidates after a non-expression rule changes', async () => {
    vi.useFakeTimers();
    apiMocks.validateFormula.mockResolvedValue({ data: { diagnostics: [], latex: 'x' } });
    const wrapper = mount(FormulaEditor, {
      attachTo: document.body,
      props: { functions, modelValue: 'sample_weight', parameters, rule: { biaodashi: 'sample_weight', jieguoLeixing: 'DECIMAL', parameters } },
      global: { stubs: { 'a-alert': true, 'a-button': true } },
    });

    await (wrapper.vm as any).validate();
    await flushPromises();
    await wrapper.setProps({ rule: { biaodashi: 'sample_weight', jieguoLeixing: 'BOOLEAN', parameters: [{ ...parameters[0]!, shifouBitian: 0 }] } });
    await nextTick();

    expect(wrapper.emitted('validation')?.at(-1)?.[0]).toEqual({ diagnostics: [], latex: null });
    await vi.advanceTimersByTimeAsync(400);
    await flushPromises();
    expect(apiMocks.validateFormula).toHaveBeenLastCalledWith(
      expect.objectContaining({ jieguoLeixing: 'BOOLEAN', parameters: [{ ...parameters[0]!, shifouBitian: 0 }] }),
      expect.any(AbortSignal),
    );
    wrapper.unmount();
  });
});
