import { describe, expect, it, jest } from '@jest/globals';
import { shutdownWorkerRuntime, startWorkerRuntime } from '../../workerLifecycle';

describe('startWorkerRuntime', () => {
  it('starts heartbeat only after config fallback settles and Bull is ready', async () => {
    const order: string[] = [];
    const initializeConfig = jest.fn(async () => {
      order.push('config');
      throw new Error('DB unavailable; env fallback');
    });
    const onConfigFallback = jest.fn(async () => {
      order.push('fallback:start');
      await Promise.resolve();
      order.push('fallback:end');
    });
    const waitForQueue = jest.fn(async () => {
      order.push('queue');
    });
    const startOutboxDrainer = jest.fn(async () => {
      order.push('outbox');
    });
    const startHeartbeat = jest.fn(() => {
      order.push('heartbeat');
    });

    await startWorkerRuntime({ initializeConfig, onConfigFallback, waitForQueue, startOutboxDrainer, startHeartbeat });

    expect(order).toEqual(['config', 'fallback:start', 'fallback:end', 'queue', 'outbox', 'heartbeat']);
    expect(startOutboxDrainer).toHaveBeenCalledTimes(1);
  });

  it('does not emit heartbeat when Bull readiness fails', async () => {
    const startHeartbeat = jest.fn();

    await expect(startWorkerRuntime({
      initializeConfig: jest.fn<() => Promise<void>>().mockResolvedValue(undefined),
      waitForQueue: jest.fn<() => Promise<void>>().mockRejectedValue(new Error('Queue unavailable')),
      startHeartbeat,
    })).rejects.toThrow('Queue unavailable');

    expect(startHeartbeat).not.toHaveBeenCalled();
  });
});

describe('shutdownWorkerRuntime', () => {
  it('stops health reporting without disconnecting Prisma when Bull misses its drain deadline', async () => {
    jest.useFakeTimers();
    const stopHeartbeat = jest.fn<() => Promise<void>>().mockResolvedValue(undefined);
    const closeConfigSubscription = jest.fn<() => Promise<void>>().mockResolvedValue(undefined);
    const disconnectDatabase = jest.fn<() => Promise<void>>().mockResolvedValue(undefined);
    const shutdown = shutdownWorkerRuntime({
      closeQueue: () => new Promise(() => undefined),
      stopHeartbeat,
      closeConfigSubscription,
      disconnectDatabase,
      drainTimeoutMs: 1000,
    });

    let error: AggregateError | undefined;
    const failure = shutdown.catch((caught) => {
      error = caught as AggregateError;
    });
    await jest.advanceTimersByTimeAsync(1000);
    await failure;
    expect(error?.errors[0]).toEqual(
      new Error('Worker queue drain timed out after 1000ms')
    );
    expect(stopHeartbeat).toHaveBeenCalledTimes(1);
    expect(closeConfigSubscription).toHaveBeenCalledTimes(1);
    expect(disconnectDatabase).not.toHaveBeenCalled();
    jest.useRealTimers();
  });

  it('drains Bull before stopping heartbeat and disconnects Prisma only after heartbeat settles', async () => {
    const order: string[] = [];
    let releaseQueue!: () => void;
    let releaseHeartbeat!: () => void;
    const queueClosed = new Promise<void>((resolve) => { releaseQueue = resolve; });
    const heartbeatStopped = new Promise<void>((resolve) => { releaseHeartbeat = resolve; });
    const closeQueue = jest.fn(async () => {
      order.push('queue:start');
      await queueClosed;
      order.push('queue:end');
    });
    const stopHeartbeat = jest.fn(async () => {
      order.push('heartbeat:start');
      await heartbeatStopped;
      order.push('heartbeat:end');
    });
    const closeConfigSubscription = jest.fn(async () => {
      order.push('config:start');
      await heartbeatStopped;
      order.push('config:end');
    });
    const disconnectDatabase = jest.fn(async () => {
      order.push('database');
    });
    const stopOutboxDrainer = jest.fn(async () => {
      order.push('dispatcher');
    });

    const shutdown = shutdownWorkerRuntime({
      closeQueue,
      stopHeartbeat,
      closeConfigSubscription,
      disconnectDatabase,
      stopOutboxDrainer,
    });
    await Promise.resolve();
    expect(order).toEqual(['dispatcher', 'queue:start']);

    releaseQueue();
    await new Promise<void>((resolve) => setImmediate(resolve));
    expect(order).toEqual(expect.arrayContaining(['queue:end', 'heartbeat:start', 'config:start']));
    expect(order).not.toContain('database');

    releaseHeartbeat();
    await shutdown;
    expect(order.at(-1)).toBe('database');
    expect(closeQueue).toHaveBeenCalledTimes(1);
    expect(stopOutboxDrainer).toHaveBeenCalledTimes(1);
    expect(stopHeartbeat).toHaveBeenCalledTimes(1);
    expect(closeConfigSubscription).toHaveBeenCalledTimes(1);
    expect(disconnectDatabase).toHaveBeenCalledTimes(1);
  });
});
