import { ConfigService } from '@nestjs/config';
import { Queue, Worker } from 'bullmq';
import { AppLoggerService } from '../logging/app-logger.service';
import { RedisService } from '../redis/redis.service';
import { AppQueueService } from './app-queue.service';

jest.mock('bullmq', () => ({ Queue: jest.fn(), Worker: jest.fn() }));

describe('AppQueueService', () => {
  const values: Record<string, unknown> = {};
  const config = { get: jest.fn((key: string) => values[key]) } as unknown as ConfigService;
  const redis = { isEnabled: jest.fn(), createQueueClient: jest.fn() } as unknown as RedisService;
  const logger = { error: jest.fn() } as unknown as AppLoggerService;
  let queueInstance: { add: jest.Mock; close: jest.Mock };
  let workerInstance: { on: jest.Mock; close: jest.Mock };
  let workerProcessor: (job: { name: string; data: unknown }) => Promise<void>;

  beforeEach(() => {
    jest.clearAllMocks();
    Object.keys(values).forEach((key) => delete values[key]);
    queueInstance = { add: jest.fn(), close: jest.fn() };
    workerInstance = { on: jest.fn(), close: jest.fn() };
    jest.mocked(Queue).mockImplementation(() => queueInstance as never);
    jest.mocked(Worker).mockImplementation((_name, processor) => {
      workerProcessor = processor as typeof workerProcessor;
      return workerInstance as never;
    });
  });

  it('does not connect when queues or Redis are disabled', () => {
    (redis.isEnabled as jest.Mock).mockReturnValue(true);
    const service = new AppQueueService(config, redis, logger);
    service.onModuleInit();
    service.onApplicationBootstrap();
    expect(Queue).not.toHaveBeenCalled();

    values['queue.enabled'] = true;
    (redis.isEnabled as jest.Mock).mockReturnValue(false);
    service.onApplicationBootstrap();
    expect(redis.createQueueClient).not.toHaveBeenCalled();
  });

  it('starts BullMQ with configured defaults and dispatches registered processors', async () => {
    Object.assign(values, {
      'queue.enabled': true,
      'queue.name': 'important-jobs',
      'queue.workerConcurrency': 8,
      'queue.defaultJobAttempts': 5,
      'queue.defaultJobBackoffMs': 250,
      'queue.removeOnComplete': false,
    });
    (redis.isEnabled as jest.Mock).mockReturnValue(true);
    (redis.createQueueClient as jest.Mock).mockReturnValueOnce({ id: 'queue' }).mockReturnValueOnce({ id: 'worker' });
    const service = new AppQueueService(config, redis, logger);
    const processor = jest.fn().mockResolvedValue(undefined);
    service.registerProcessor('send', processor);
    service.onApplicationBootstrap();

    expect(Queue).toHaveBeenCalledWith('important-jobs', expect.objectContaining({ defaultJobOptions: { attempts: 5, backoff: { type: 'exponential', delay: 250 }, removeOnComplete: false } }));
    expect(Worker).toHaveBeenCalledWith('important-jobs', expect.any(Function), expect.objectContaining({ concurrency: 8 }));
    await workerProcessor({ name: 'send', data: { id: 1 } });
    expect(processor).toHaveBeenCalledWith({ id: 1 });
    await expect(workerProcessor({ name: 'missing', data: {} })).rejects.toThrow('No processor registered');

    const failed = workerInstance.on.mock.calls.find(([event]) => event === 'failed')?.[1] as (
      job: { name: string; id: string },
      error: Error,
    ) => void;
    failed({ name: 'send', id: 'j1' }, new Error('boom'));
    expect(logger.error).toHaveBeenCalledWith(expect.objectContaining({ queue: 'important-jobs', jobId: 'j1', error: 'boom' }), undefined, 'AppQueueService');
  });

  it('enqueues remotely with per-job overrides and closes resources', async () => {
    values['queue.enabled'] = true;
    (redis.isEnabled as jest.Mock).mockReturnValue(true);
    (redis.createQueueClient as jest.Mock).mockReturnValue({});
    const service = new AppQueueService(config, redis, logger);
    service.onApplicationBootstrap();
    await service.enqueue('job', { id: 1 }, { attempts: 9 });
    expect(queueInstance.add).toHaveBeenCalledWith('job', { id: 1 }, expect.objectContaining({ attempts: 9, removeOnComplete: true }));
    await service.onModuleDestroy();
    expect(workerInstance.close).toHaveBeenCalled();
    expect(queueInstance.close).toHaveBeenCalled();
  });

  it('falls back to a microtask and logs processor failures', async () => {
    const service = new AppQueueService(config, redis, logger);
    const success = jest.fn().mockResolvedValue(undefined);
    service.registerProcessor('success', success);
    await service.enqueue('success', { ok: true });
    await new Promise((resolve) => setImmediate(resolve));
    expect(success).toHaveBeenCalledWith({ ok: true });

    const failed = jest.fn().mockRejectedValue(new Error('offline'));
    service.registerProcessor('failed', failed);
    await service.enqueue('failed', { id: 4 });
    await new Promise((resolve) => setImmediate(resolve));
    expect(logger.error).toHaveBeenCalledWith(expect.objectContaining({ jobName: 'failed', error: 'offline' }), expect.any(String), 'AppQueueService');
    await expect(service.enqueue('unknown', {})).resolves.toBeUndefined();
  });

  it('aborts startup when either Redis connection cannot be created', () => {
    values['queue.enabled'] = true;
    (redis.isEnabled as jest.Mock).mockReturnValue(true);
    (redis.createQueueClient as jest.Mock).mockReturnValueOnce({}).mockReturnValueOnce(null);
    new AppQueueService(config, redis, logger).onApplicationBootstrap();
    expect(Queue).not.toHaveBeenCalled();
  });
});
