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

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

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

import { useFormulaValidation } from '../useFormulaValidation';

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

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

function deferred<T>() {
  let resolve!: (value: T) => void;
  const promise = new Promise<T>((promiseResolve) => {
    resolve = promiseResolve;
  });
  return { promise, resolve };
}

function rule(expression = 'a + b'): FormulaRule {
  return { biaodashi: expression, jieguoLeixing: 'DECIMAL', parameters: [] };
}

function mountValidation() {
  let validation!: ReturnType<typeof useFormulaValidation>;
  const wrapper = mount(
    defineComponent({
      setup() {
        validation = useFormulaValidation();
        return () => null;
      },
    }),
  );
  return { validation, wrapper };
}

describe('useFormulaValidation', () => {
  afterEach(() => {
    vi.useRealTimers();
    vi.clearAllMocks();
  });

  it('waits 400ms before validating a scheduled edit', async () => {
    vi.useFakeTimers();
    apiMocks.validateFormula.mockResolvedValue({ data: { diagnostics: [], latex: 'a+b' } });
    const { validation } = mountValidation();

    validation.schedule(rule());
    await vi.advanceTimersByTimeAsync(399);
    expect(apiMocks.validateFormula).not.toHaveBeenCalled();
    await vi.advanceTimersByTimeAsync(1);
    await flushPromises();

    expect(apiMocks.validateFormula).toHaveBeenCalledTimes(1);
  });

  it('aborts an active request when a newer validation starts', async () => {
    const first = deferred<any>();
    const second = deferred<any>();
    apiMocks.validateFormula
      .mockImplementationOnce((_rule: FormulaRule, signal: AbortSignal) => {
        expect(signal.aborted).toBe(false);
        return first.promise;
      })
      .mockImplementationOnce(() => second.promise);
    const { validation } = mountValidation();

    void validation.validateNow(rule('a'));
    await Promise.resolve();
    void validation.validateNow(rule('b'));
    await Promise.resolve();

    expect(apiMocks.validateFormula.mock.calls[0]![1]!.aborted).toBe(true);
    second.resolve({ data: { diagnostics: [], latex: 'b' } });
    first.resolve({ data: { diagnostics: [], latex: 'a' } });
    await flushPromises();
  });

  it('drops a late response from an older request', async () => {
    const first = deferred<any>();
    const second = deferred<any>();
    apiMocks.validateFormula.mockImplementationOnce(() => first.promise).mockImplementationOnce(() => second.promise);
    const { validation } = mountValidation();

    void validation.validateNow(rule('a'));
    void validation.validateNow(rule('b'));
    second.resolve({ data: { diagnostics: [], latex: 'b' } });
    await flushPromises();
    first.resolve({ data: { diagnostics: [], latex: 'a' } });
    await flushPromises();

    expect(validation.result.value?.latex).toBe('b');
  });

  it('runs a manual validation immediately and cancels the pending debounce', async () => {
    vi.useFakeTimers();
    apiMocks.validateFormula.mockResolvedValue({ data: { diagnostics: [], latex: 'manual' } });
    const { validation } = mountValidation();

    validation.schedule(rule('scheduled'));
    await validation.validateNow(rule('manual'));
    await vi.advanceTimersByTimeAsync(400);

    expect(apiMocks.validateFormula).toHaveBeenCalledTimes(1);
    expect(apiMocks.validateFormula).toHaveBeenCalledWith(rule('manual'), expect.any(AbortSignal));
  });

  it('invalidates an active validation as soon as a new edit enters the debounce window', async () => {
    vi.useFakeTimers();
    const oldResponse = deferred<any>();
    apiMocks.validateFormula.mockImplementationOnce((_rule: FormulaRule, signal: AbortSignal) => {
      expect(signal.aborted).toBe(false);
      return oldResponse.promise;
    });
    const { validation } = mountValidation();

    void validation.validateNow(rule('old_expression'));
    await Promise.resolve();
    const oldSignal = apiMocks.validateFormula.mock.calls[0]![1] as AbortSignal;
    validation.schedule(rule('new_expression'));
    oldResponse.resolve({ data: { diagnostics: [], latex: 'old_latex' } });
    await flushPromises();

    expect(oldSignal.aborted).toBe(true);
    expect(validation.result.value).toBeUndefined();
  });

  it('keeps a visible retryable failure state instead of rejecting a scheduled validation', async () => {
    apiMocks.validateFormula
      .mockRejectedValueOnce(new Error('validation service unavailable'))
      .mockResolvedValueOnce({ data: { diagnostics: [], latex: 'retry' } });
    const { validation } = mountValidation();

    await expect(validation.validateNow(rule('first'))).resolves.toBeUndefined();

    expect(validation.error.value).toBe('validation service unavailable');
    expect(validation.result.value).toEqual({ diagnostics: [], latex: null });
    await validation.retry();

    expect(apiMocks.validateFormula).toHaveBeenCalledTimes(2);
    expect(validation.error.value).toBe('');
    expect(validation.result.value?.latex).toBe('retry');
  });

  it('clears its timer and aborts an active request on unmount', async () => {
    vi.useFakeTimers();
    const pending = deferred<any>();
    apiMocks.validateFormula.mockReturnValue(pending.promise);
    const { validation, wrapper } = mountValidation();

    validation.schedule(rule('scheduled'));
    void validation.validateNow(rule('active'));
    await Promise.resolve();
    const signal = apiMocks.validateFormula.mock.calls[0]![1] as AbortSignal;
    wrapper.unmount();
    await vi.advanceTimersByTimeAsync(400);

    expect(signal.aborted).toBe(true);
    expect(apiMocks.validateFormula).toHaveBeenCalledTimes(1);
  });
});
