import type { FormulaRule } from '#/api/x/eln/formulas';

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

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

import TrialPanel, { parseDecimalListInput } from '../TrialPanel.vue';
import panelSource from '../TrialPanel.vue?raw';

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

vi.mock('#/api/x/eln/formulas', () => ({ previewFormula: apiMocks.previewFormula }));

const stubs = {
  'a-alert': { props: ['message'], template: '<div>{{ message }}</div>' },
  'a-button': defineComponent({
    props: ['disabled', 'loading'],
    emits: ['click'],
    template: '<button :disabled="disabled" @click="$emit(\'click\')"><slot /></button>',
  }),
  'a-descriptions': { template: '<div><slot /></div>' },
  'a-descriptions-item': { template: '<span><slot /></span>' },
  'a-input': defineComponent({
    props: ['value'],
    emits: ['update:value'],
    template: '<input class="trial-text" :value="value" @input="$emit(\'update:value\', $event.target.value)" />',
  }),
  'a-input-number': defineComponent({
    props: ['value'],
    emits: ['update:value'],
    template: '<input class="trial-number" :value="value" @input="$emit(\'update:value\', Number($event.target.value))" />',
  }),
  'a-select': defineComponent({
    name: 'ASelect',
    props: ['fieldNames', 'options', 'value'],
    emits: ['update:value'],
    template: '<div class="trial-boolean">{{ value }}</div>',
  }),
  'a-textarea': defineComponent({
    props: ['value'],
    emits: ['update:value'],
    template: '<textarea class="trial-list" :value="value" @input="$emit(\'update:value\', $event.target.value)" />',
  }),
};

const rule: FormulaRule = {
  biaodashi: 'amount + flag',
  jieguoLeixing: 'DECIMAL',
  parameters: [
    { canshuBianma: 'amount', canshuMingcheng: '含量', paixu: 1, shifouBitian: 1, shujuLeixing: 'DECIMAL' },
    { canshuBianma: 'replicates', canshuMingcheng: '重复值', paixu: 2, shifouBitian: 1, shujuLeixing: 'DECIMAL_LIST' },
    { canshuBianma: 'remark', canshuMingcheng: '备注', paixu: 3, shifouBitian: 0, shujuLeixing: 'TEXT' },
    { canshuBianma: 'flag', canshuMingcheng: '是否合格', paixu: 4, shifouBitian: 1, shujuLeixing: 'BOOLEAN' },
  ],
};

describe('eLN formula trial panel', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('parses decimal-list values line by line and rejects invalid values', () => {
    expect(parseDecimalListInput('1.2\n-3\n')).toEqual(['1.2', '-3']);
    expect(() => parseDecimalListInput('1\n1e3')).toThrow('第 2 行不是有效十进制数');
  });

  it('keeps typed inputs locally and calls the preview API with all slot types', async () => {
    apiMocks.previewFormula.mockResolvedValue({ data: { diagnostics: [], rawResult: '3.0', roundedResult: '3.00', status: 'SUCCESS' } });
    const wrapper = mount(TrialPanel, { props: { rule }, global: { stubs } });

    await wrapper.find('.trial-number').setValue('1.25');
    await wrapper.find('.trial-list').setValue('1\n2.5');
    await wrapper.find('.trial-text').setValue('备注');
    await wrapper.findComponent({ name: 'ASelect' }).vm.$emit('update:value', false);
    await wrapper.find('[data-test="run-trial"]').trigger('click');
    await flushPromises();

    expect(apiMocks.previewFormula).toHaveBeenCalledWith({
      ...rule,
      inputs: { amount: 1.25, flag: false, remark: '备注', replicates: ['1', '2.5'] },
    });
    expect(wrapper.text()).toContain('原始结果：3.0');
    expect(wrapper.text()).toContain('修约结果：3.00');
    expect(panelSource).toContain("slot.shujuLeixing === 'DECIMAL'");
    expect(panelSource).toContain("slot.shujuLeixing === 'DECIMAL_LIST'");
    expect(panelSource).toContain("slot.shujuLeixing === 'TEXT'");
    expect(panelSource).toContain("slot.shujuLeixing === 'BOOLEAN'");
    expect(panelSource).not.toContain('localStorage');
    expect(panelSource).not.toContain('sessionStorage');
  });

  it('shows missing-input status and diagnostics while leaving results empty', async () => {
    apiMocks.previewFormula.mockResolvedValue({
      data: {
        diagnostics: [{ end: 6, message: '缺少参数：amount', severity: 'ERROR', start: 0 }],
        rawResult: null,
        roundedResult: null,
        status: 'MISSING_INPUT',
      },
    });
    const wrapper = mount(TrialPanel, { props: { rule }, global: { stubs } });

    await wrapper.find('[data-test="run-trial"]').trigger('click');
    await flushPromises();
    expect(apiMocks.previewFormula).toHaveBeenCalledTimes(1);
    expect(wrapper.text()).toContain('MISSING_INPUT');
    expect(wrapper.text()).toContain('缺少参数：amount');
    expect(wrapper.text()).not.toContain('原始结果');
    expect(wrapper.text()).not.toContain('修约结果');
  });

  it('submits an untouched boolean parameter as null instead of false', async () => {
    apiMocks.previewFormula.mockResolvedValue({ data: { diagnostics: [], rawResult: null, roundedResult: null, status: 'MISSING_INPUT' } });
    const wrapper = mount(TrialPanel, { props: { rule }, global: { stubs } });

    await wrapper.find('[data-test="run-trial"]').trigger('click');
    await flushPromises();

    expect(apiMocks.previewFormula).toHaveBeenCalledWith(expect.objectContaining({ inputs: expect.objectContaining({ flag: null }) }));
  });

  it('shows error status and diagnostics while leaving results empty', async () => {
    apiMocks.previewFormula.mockResolvedValue({
      data: { diagnostics: [{ end: 4, message: '除数不能为零', severity: 'ERROR', start: 2 }], rawResult: null, roundedResult: null, status: 'ERROR' },
    });
    const wrapper = mount(TrialPanel, { props: { rule }, global: { stubs } });

    await wrapper.find('[data-test="run-trial"]').trigger('click');
    await flushPromises();

    expect(wrapper.text()).toContain('ERROR');
    expect(wrapper.text()).toContain('除数不能为零');
    expect(wrapper.text()).not.toContain('原始结果');
    expect(wrapper.text()).not.toContain('修约结果');
  });

  it('clears trial inputs and marks an existing result stale after rule changes', async () => {
    apiMocks.previewFormula.mockResolvedValue({ data: { diagnostics: [], rawResult: '1.25', roundedResult: '1.25', status: 'SUCCESS' } });
    const wrapper = mount(TrialPanel, { props: { rule }, global: { stubs } });
    await wrapper.find('.trial-number').setValue('1.25');
    await wrapper.find('[data-test="run-trial"]').trigger('click');
    await flushPromises();

    await wrapper.setProps({ rule: { ...rule, biaodashi: 'amount * 2' } });
    await nextTick();
    expect(wrapper.text()).toContain('结果已过期');
    expect(wrapper.text()).toContain('原始结果：1.25');
    expect(apiMocks.previewFormula).toHaveBeenCalledTimes(1);

    await wrapper.find('[data-test="clear-trial-inputs"]').trigger('click');
    expect((wrapper.find('.trial-number').element as HTMLInputElement).value).toBe('');
  });

  it('removes inputs for deleted parameters before the next trial', async () => {
    apiMocks.previewFormula.mockResolvedValue({ data: { diagnostics: [], rawResult: null, roundedResult: null, status: 'MISSING_INPUT' } });
    const wrapper = mount(TrialPanel, { props: { rule }, global: { stubs } });
    await wrapper.find('.trial-text').setValue('将被删除');
    await wrapper.setProps({ rule: { ...rule, parameters: rule.parameters.filter((slot) => slot.canshuBianma !== 'remark') } });
    await nextTick();
    await wrapper.find('[data-test="run-trial"]').trigger('click');
    await flushPromises();

    expect(apiMocks.previewFormula).toHaveBeenCalledWith(expect.objectContaining({ inputs: expect.not.objectContaining({ remark: expect.anything() }) }));
  });

  it('prevents duplicate submissions and keeps inputs when the request fails', async () => {
    let rejectRequest!: (error: Error) => void;
    apiMocks.previewFormula.mockReturnValue(
      new Promise((_resolve, reject) => {
        rejectRequest = reject;
      }),
    );
    const wrapper = mount(TrialPanel, { props: { rule }, global: { stubs } });
    await wrapper.find('.trial-number').setValue('2');
    void wrapper.find('[data-test="run-trial"]').trigger('click');
    await wrapper.find('[data-test="run-trial"]').trigger('click');
    expect(apiMocks.previewFormula).toHaveBeenCalledTimes(1);

    rejectRequest(new Error('network'));
    await flushPromises();
    expect(wrapper.text()).toContain('试算请求失败');
    expect((wrapper.find('.trial-number').element as HTMLInputElement).value).toBe('2');
  });
});
