import { ConfigService } from '@nestjs/config';
import { Queue } from 'bullmq';
import {
  AUTOMATION_QUEUE_NAME,
  AutomationJobData,
} from './automation-job-data';
import { BullMqAutomationQueuePort } from './bullmq-queue.port';

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

const jobData: AutomationJobData = {
  runId: 'run_1',
  automationId: 'automation_1',
  versionId: 'version_1',
  nodeKey: 'trigger_1',
  subject: {},
  payload: {},
  dryRun: true,
  attempt: 1,
};

describe('BullMqAutomationQueuePort', () => {
  const QueueMock = Queue as jest.MockedClass<typeof Queue>;

  beforeEach(() => {
    jest.clearAllMocks();
  });

  it('creates a BullMQ job when REDIS_URL is configured', async () => {
    const add = jest.fn().mockResolvedValue({ id: 'job_1' });
    const close = jest.fn();
    QueueMock.mockImplementationOnce(() => ({ add, close }) as never);

    const port = new BullMqAutomationQueuePort(
      configService({
        REDIS_URL: 'redis://localhost:6379',
        AUTOMATION_QUEUE_PREFIX: 'planfi:automations',
      }),
    );

    await expect(port.enqueue(jobData, 60_000)).resolves.toEqual({
      id: 'job_1',
      degraded: false,
    });
    expect(port.isAvailable()).toBe(true);
    expect(QueueMock).toHaveBeenCalledWith(
      AUTOMATION_QUEUE_NAME,
      expect.objectContaining({
        prefix: 'planfi:automations',
      }),
    );
    expect(add).toHaveBeenCalledWith(
      'execute-node',
      jobData,
      expect.objectContaining({
        delay: 60_000,
        jobId: 'execute-node__run_1__trigger_1__1',
      }),
    );
  });

  it('degrades without creating a BullMQ queue when REDIS_URL is missing', async () => {
    const port = new BullMqAutomationQueuePort(configService({}));

    await expect(port.enqueue(jobData)).resolves.toEqual({
      id: null,
      degraded: true,
    });
    expect(port.isAvailable()).toBe(false);
    expect(QueueMock).not.toHaveBeenCalled();
  });

  // ARFIX-09: jobId de resume-atlas único por entrada de Espera
  it('resume-atlas com waitSeq distintos para o mesmo runId+nodeKey gera jobIds distintos', async () => {
    const capturedJobIds: string[] = [];
    const add = jest.fn().mockImplementation((_name: string, _data: unknown, opts: { jobId?: string }) => {
      capturedJobIds.push(opts.jobId ?? '');
      return Promise.resolve({ id: 'job_x' });
    });
    const close = jest.fn();
    QueueMock.mockImplementationOnce(() => ({ add, close }) as never);

    const port = new BullMqAutomationQueuePort(
      configService({ REDIS_URL: 'redis://localhost:6379' }),
    );

    const baseData: AutomationJobData = {
      kind: 'resume-atlas',
      runId: 'run_loop',
      automationId: 'run_loop',
      versionId: null,
      nodeKey: 'wait_node_1',
      subject: {},
      payload: {},
      dryRun: false,
      attempt: 1,
    };

    await port.enqueue({ ...baseData, waitSeq: 'wait-log-001' }, 0);
    await port.enqueue({ ...baseData, waitSeq: 'wait-log-002' }, 0);

    expect(capturedJobIds).toHaveLength(2);
    expect(capturedJobIds[0]).not.toBe(capturedJobIds[1]);
    expect(capturedJobIds[0]).toContain('resume-atlas__run_loop__wait_node_1__wait-log-001');
    expect(capturedJobIds[1]).toContain('resume-atlas__run_loop__wait_node_1__wait-log-002');
  });

  it('resume-atlas sem waitSeq usa fallback (UUID) — jobId não colide entre chamadas', async () => {
    const capturedJobIds: string[] = [];
    const add = jest.fn().mockImplementation((_name: string, _data: unknown, opts: { jobId?: string }) => {
      capturedJobIds.push(opts.jobId ?? '');
      return Promise.resolve({ id: 'job_y' });
    });
    const close = jest.fn();
    QueueMock.mockImplementationOnce(() => ({ add, close }) as never);

    const port = new BullMqAutomationQueuePort(
      configService({ REDIS_URL: 'redis://localhost:6379' }),
    );

    const noSeqData: AutomationJobData = {
      kind: 'resume-atlas',
      runId: 'run_fallback',
      automationId: 'run_fallback',
      versionId: null,
      nodeKey: 'resume-atlas',
      subject: {},
      payload: {},
      dryRun: false,
      attempt: 1,
      // waitSeq ausente → fallback UUID
    };

    await port.enqueue(noSeqData, 0);
    await port.enqueue(noSeqData, 0);

    expect(capturedJobIds).toHaveLength(2);
    // Com UUID como fallback, os dois jobIds são diferentes (não colisão)
    expect(capturedJobIds[0]).not.toBe(capturedJobIds[1]);
    expect(capturedJobIds[0]).toMatch(/^resume-atlas__run_fallback__resume-atlas__/);
  });

  it('execute-node jobId formula permanece inalterada', async () => {
    const add = jest.fn().mockResolvedValue({ id: 'job_1' });
    const close = jest.fn();
    QueueMock.mockImplementationOnce(() => ({ add, close }) as never);

    const port = new BullMqAutomationQueuePort(
      configService({
        REDIS_URL: 'redis://localhost:6379',
        AUTOMATION_QUEUE_PREFIX: 'planfi:automations',
      }),
    );

    await port.enqueue(jobData, 60_000);

    expect(add).toHaveBeenCalledWith(
      'execute-node',
      jobData,
      expect.objectContaining({
        jobId: 'execute-node__run_1__trigger_1__1',
      }),
    );
  });
});

function configService(values: Record<string, string>): ConfigService {
  return {
    get: jest.fn((key: string) => values[key]),
  } as unknown as ConfigService;
}
