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

import { createHeartbeatController } from '../../src/heartbeat-controller';

describe('heartbeat controller', () => {
  beforeEach(() => vi.useFakeTimers());
  afterEach(() => vi.useRealTimers());

  test('does not overlap ticks while a heartbeat is in flight', async () => {
    let resolveHeartbeat!: (value: { cancelRequested: boolean }) => void;
    const heartbeat = vi.fn(
      () =>
        new Promise<{ cancelRequested: boolean }>((resolve) => {
          resolveHeartbeat = resolve;
        }),
    );
    const controller = createHeartbeatController({
      intervalMs: 30_000,
      heartbeat,
      onCancel: vi.fn(),
      onError: vi.fn(),
    });

    controller.start('job-1');
    await vi.advanceTimersByTimeAsync(90_000);
    expect(heartbeat).toHaveBeenCalledOnce();

    resolveHeartbeat({ cancelRequested: false });
    await Promise.resolve();
    await vi.advanceTimersByTimeAsync(30_000);
    expect(heartbeat).toHaveBeenCalledTimes(2);
    controller.stop('job-1');
  });

  test('removes the timer before awaiting cancellation teardown', async () => {
    let finishCancel!: () => void;
    const onCancel = vi.fn(
      () =>
        new Promise<void>((resolve) => {
          finishCancel = resolve;
        }),
    );
    const heartbeat = vi.fn().mockResolvedValue({ cancelRequested: true });
    const controller = createHeartbeatController({
      intervalMs: 30_000,
      heartbeat,
      onCancel,
      onError: vi.fn(),
    });

    controller.start('job-1');
    await vi.advanceTimersByTimeAsync(30_000);
    expect(onCancel).toHaveBeenCalledOnce();
    expect(controller.has('job-1')).toBe(false);

    await vi.advanceTimersByTimeAsync(90_000);
    expect(heartbeat).toHaveBeenCalledOnce();
    finishCancel();
  });
});
