import { ConfigService } from '@nestjs/config';
import { AtlasRuntimeClient } from '../atlas-runtime.client';
import { MessageProviderRegistry } from '../providers/message-providers';
import { AutomationProcessor } from './automation.processor';
import { AutomationQueueService } from './automation-queue.service';

describe('AutomationProcessor', () => {
  const configGetMock = jest.fn((key: string) => {
    const values: Record<string, unknown> = {
      AUTOMATION_SEND_ENABLED: false,
      AUTOMATION_DRY_RUN: true,
    };
    return values[key];
  });
  const configService = {
    get: configGetMock,
  } as unknown as ConfigService;
  const atlasRuntime = {
    getDefinition: jest.fn(),
    getRunStatus: jest.fn(),
    reportRuntimeEvent: jest.fn(),
    checkOptOut: jest.fn(),
    sendMessage: jest.fn(),
    resumeRun: jest.fn(),
  };
  const queueService = {
    enqueue: jest.fn(),
  };

  beforeEach(() => {
    jest.clearAllMocks();
    configGetMock.mockImplementation((key: string) => {
      const values: Record<string, unknown> = {
        AUTOMATION_SEND_ENABLED: false,
        AUTOMATION_DRY_RUN: true,
      };
      return values[key];
    });
    atlasRuntime.getRunStatus.mockResolvedValue({
      id: 'run_1',
      status: 'RUNNING',
    });
    atlasRuntime.reportRuntimeEvent.mockResolvedValue({});
    atlasRuntime.checkOptOut.mockResolvedValue(false);
    atlasRuntime.sendMessage.mockResolvedValue({
      success: true,
      status: 'sent',
      retryable: false,
    });
    queueService.enqueue.mockResolvedValue({ id: 'job_1', degraded: false });
  });

  function processor(
    providers = new MessageProviderRegistry(
      configService,
      atlasRuntime as unknown as AtlasRuntimeClient,
    ),
  ) {
    return new AutomationProcessor(
      configService,
      atlasRuntime as unknown as AtlasRuntimeClient,
      queueService as unknown as AutomationQueueService,
      providers,
    );
  }

  it('job resume-atlas pede retomada ao Atlas e não toca o engine', async () => {
    atlasRuntime.resumeRun.mockResolvedValue({ result: {} });

    await processor().processJob({
      kind: 'resume-atlas',
      runId: 'run_1',
      automationId: 'run_1',
      versionId: null,
      nodeKey: 'resume-atlas',
      subject: {},
      payload: {},
      dryRun: false,
      attempt: 1,
    });

    expect(atlasRuntime.resumeRun).toHaveBeenCalledWith('run_1');
    expect(atlasRuntime.getRunStatus).not.toHaveBeenCalled();
    expect(atlasRuntime.getDefinition).not.toHaveBeenCalled();
  });

  it('WAIT em dry-run é colapsado e não agenda delay', async () => {
    atlasRuntime.getDefinition.mockResolvedValue({
      automation: { id: 'auto_1' },
      version: { id: 'ver_1' },
      nodes: [
        {
          nodeKey: 'wait_1',
          type: 'WAIT',
          label: 'Esperar 15 minutos',
          positionX: 0,
          positionY: 0,
          config: { duration: 15, unit: 'minutes' },
        },
        {
          nodeKey: 'end_1',
          type: 'END',
          label: 'Fim',
          positionX: 0,
          positionY: 100,
          config: {},
        },
      ],
      edges: [
        { edgeKey: 'e1', sourceNodeKey: 'wait_1', targetNodeKey: 'end_1' },
      ],
    });

    await processor().processJob({
      runId: 'run_1',
      automationId: 'auto_1',
      versionId: 'ver_1',
      nodeKey: 'wait_1',
      subject: {},
      payload: {},
      dryRun: true,
      attempt: 1,
    });

    expect(queueService.enqueue).toHaveBeenCalledWith(
      expect.objectContaining({ nodeKey: 'end_1' }),
      0,
    );

    const runtimeEvent = lastReportRuntimeEvent();
    expect(runtimeEvent).toMatchObject({
      runStatus: 'RUNNING',
      log: {
        event: 'wait.skipped',
        status: 'WAITING',
      },
    });
  });

  it('WAIT real vira job atrasado no BullMQ', async () => {
    atlasRuntime.getDefinition.mockResolvedValue({
      automation: { id: 'auto_1' },
      version: { id: 'ver_1' },
      nodes: [
        {
          nodeKey: 'wait_1',
          type: 'WAIT',
          label: 'Esperar 15 minutos',
          positionX: 0,
          positionY: 0,
          config: { duration: 15, unit: 'minutes' },
        },
        {
          nodeKey: 'end_1',
          type: 'END',
          label: 'Fim',
          positionX: 0,
          positionY: 100,
          config: {},
        },
      ],
      edges: [
        { edgeKey: 'e1', sourceNodeKey: 'wait_1', targetNodeKey: 'end_1' },
      ],
    });

    await processor().processJob({
      runId: 'run_1',
      automationId: 'auto_1',
      versionId: 'ver_1',
      nodeKey: 'wait_1',
      subject: {},
      payload: {},
      dryRun: false,
      attempt: 1,
    });

    expect(queueService.enqueue).toHaveBeenCalledWith(
      expect.objectContaining({ nodeKey: 'end_1' }),
      15 * 60_000,
    );

    const runtimeEvent = lastReportRuntimeEvent();
    expect(runtimeEvent).toMatchObject({
      runStatus: 'WAITING',
      log: {
        event: 'wait.scheduled',
        status: 'WAITING',
      },
    });
  });

  it('ACTION mensagem em dry-run reporta provider dry_run', async () => {
    atlasRuntime.getDefinition.mockResolvedValue({
      automation: { id: 'auto_1' },
      version: { id: 'ver_1' },
      nodes: [
        {
          nodeKey: 'action_1',
          type: 'ACTION',
          label: 'Enviar e-mail',
          positionX: 0,
          positionY: 0,
          config: { channel: 'EMAIL', body: 'Olá {{nome}}' },
        },
      ],
      edges: [],
    });

    await processor().processJob({
      runId: 'run_1',
      automationId: 'auto_1',
      versionId: 'ver_1',
      nodeKey: 'action_1',
      subject: { userId: 'user_1' },
      payload: { nome: 'Ana', email: 'ana@planfi.com.br' },
      dryRun: true,
      attempt: 1,
    });

    expect(atlasRuntime.checkOptOut).toHaveBeenCalledWith({
      contactId: null,
      userId: 'user_1',
      channel: 'EMAIL',
    });
    const runtimeEvent = reportRuntimeEvents().find(
      (event) =>
        (event.log as Record<string, unknown> | undefined)?.event ===
        'message.sent',
    );
    expect(runtimeEvent).toMatchObject({
      log: {
        event: 'message.sent',
        status: 'SUCCESS',
      },
    });

    expect(runtimeEvent).toBeDefined();
    const log = (runtimeEvent as Record<string, unknown>).log as Record<
      string,
      unknown
    >;
    const providerResponse = log.providerResponse as Record<string, unknown>;
    expect(providerResponse.status).toBe('dry_run');
  });

  it('encerra o run quando o bloco executado não tem próximo nó', async () => {
    atlasRuntime.getDefinition.mockResolvedValue({
      automation: { id: 'auto_1' },
      version: { id: 'ver_1' },
      nodes: [
        {
          nodeKey: 'action_1',
          type: 'ACTION',
          label: 'Criar tarefa',
          positionX: 0,
          positionY: 0,
          config: { title: 'Tarefa interna' },
        },
      ],
      edges: [],
    });

    await processor().processJob({
      runId: 'run_1',
      automationId: 'auto_1',
      versionId: 'ver_1',
      nodeKey: 'action_1',
      subject: {},
      payload: {},
      dryRun: true,
      attempt: 1,
    });

    expect(atlasRuntime.reportRuntimeEvent).toHaveBeenLastCalledWith(
      expect.objectContaining({
        runId: 'run_1',
        runStatus: 'SUCCESS',
      }),
    );
    expect(typeof lastReportRuntimeEvent().finishedAt).toBe('string');
  });

  it('não processa jobs de runs cancelados', async () => {
    atlasRuntime.getRunStatus.mockResolvedValue({
      id: 'run_1',
      status: 'CANCELLED',
    });

    await processor().processJob({
      runId: 'run_1',
      automationId: 'auto_1',
      versionId: 'ver_1',
      nodeKey: 'action_1',
      subject: {},
      payload: {},
      dryRun: true,
      attempt: 1,
    });

    expect(atlasRuntime.getDefinition).not.toHaveBeenCalled();
    expect(atlasRuntime.reportRuntimeEvent).not.toHaveBeenCalled();
    expect(queueService.enqueue).not.toHaveBeenCalled();
  });

  it('registra RETRY com nextRetryAt antes de relançar erro retryable', async () => {
    atlasRuntime.getDefinition.mockResolvedValue({
      automation: { id: 'auto_1' },
      version: { id: 'ver_1' },
      nodes: [
        {
          nodeKey: 'action_1',
          type: 'ACTION',
          label: 'Enviar e-mail',
          positionX: 0,
          positionY: 0,
          config: { channel: 'EMAIL', body: 'Olá {{nome}}' },
        },
      ],
      edges: [],
    });
    const providers = {
      getProvider: jest.fn(() => ({
        send: jest.fn().mockResolvedValue({
          success: false,
          status: 'failed',
          errorCode: 'SMTP_SEND_FAILED',
          errorMessage: 'SMTP fora do ar',
          retryable: true,
        }),
      })),
    } as unknown as MessageProviderRegistry;

    await expect(
      processor(providers).processJob({
        runId: 'run_1',
        automationId: 'auto_1',
        versionId: 'ver_1',
        nodeKey: 'action_1',
        subject: { userId: 'user_1' },
        payload: { nome: 'Ana', email: 'ana@planfi.com.br' },
        dryRun: false,
        attempt: 1,
      }),
    ).rejects.toThrow('SMTP fora do ar');

    const runtimeEvent = lastReportRuntimeEvent();
    expect(runtimeEvent).toMatchObject({
      runStatus: 'WAITING',
      log: {
        event: 'message.error',
        status: 'RETRY',
        errorCode: 'SMTP_SEND_FAILED',
      },
    });
    const log = runtimeEvent.log as Record<string, unknown>;
    expect(typeof log.nextRetryAt).toBe('string');
  });

  it('registra erro retryable quando a delegação de envio para o Atlas falha', async () => {
    configGetMock.mockImplementation((key: string) => {
      const values: Record<string, unknown> = {
        AUTOMATION_SEND_ENABLED: true,
        AUTOMATION_DRY_RUN: false,
      };
      return values[key];
    });
    atlasRuntime.sendMessage.mockRejectedValueOnce(
      new Error('Service key denied: route_denied'),
    );
    atlasRuntime.getDefinition.mockResolvedValue({
      automation: { id: 'auto_1' },
      version: { id: 'ver_1' },
      nodes: [
        {
          nodeKey: 'action_1',
          type: 'ACTION',
          label: 'Enviar e-mail',
          positionX: 0,
          positionY: 0,
          config: { channel: 'EMAIL', body: 'Olá {{nome}}' },
        },
      ],
      edges: [],
    });

    await expect(
      processor().processJob({
        runId: 'run_1',
        automationId: 'auto_1',
        versionId: 'ver_1',
        nodeKey: 'action_1',
        subject: { userId: 'user_1' },
        payload: { nome: 'Ana', email: 'ana@planfi.com.br' },
        dryRun: false,
        attempt: 1,
      }),
    ).rejects.toThrow('Service key denied: route_denied');

    const runtimeEvent = lastReportRuntimeEvent();
    expect(runtimeEvent).toMatchObject({
      runStatus: 'WAITING',
      log: {
        event: 'message.error',
        status: 'RETRY',
        errorCode: 'ATLAS_PROVIDER_DELEGATION_FAILED',
        errorMessage: 'Service key denied: route_denied',
      },
    });
    const log = runtimeEvent.log as Record<string, unknown>;
    expect(typeof log.nextRetryAt).toBe('string');
  });

  it('bloco de tipo desconhecido encerra o run como FAILED sem lançar', async () => {
    atlasRuntime.getDefinition.mockResolvedValue({
      automation: { id: 'auto_1' },
      version: { id: 'ver_1' },
      nodes: [
        {
          nodeKey: 'mystery_1',
          type: 'FOO',
          label: 'Bloco estranho',
          positionX: 0,
          positionY: 0,
          config: {},
        } as unknown as {
          nodeKey: string;
          type: 'TRIGGER';
          label: string;
          positionX: number;
          positionY: number;
          config: Record<string, unknown>;
        },
      ],
      edges: [],
    });

    await expect(
      processor().processJob({
        runId: 'run_1',
        automationId: 'auto_1',
        versionId: 'ver_1',
        nodeKey: 'mystery_1',
        subject: {},
        payload: {},
        dryRun: true,
        attempt: 1,
      }),
    ).resolves.toBeUndefined();

    const failedEvent = reportRuntimeEvents().find(
      (event) =>
        (event.log as Record<string, unknown> | undefined)?.errorCode ===
        'UNKNOWN_NODE_TYPE',
    );
    expect(failedEvent).toMatchObject({
      runStatus: 'FAILED',
      log: {
        event: 'runtime.error',
        status: 'ERROR',
        errorCode: 'UNKNOWN_NODE_TYPE',
      },
    });
    expect(queueService.enqueue).not.toHaveBeenCalled();
    const successEvent = reportRuntimeEvents().find(
      (event) => event.runStatus === 'SUCCESS',
    );
    expect(successEvent).toBeUndefined();
  });

  it('envio real sem destinatário falha rápido com MISSING_RECIPIENT', async () => {
    configGetMock.mockImplementation((key: string) => {
      const values: Record<string, unknown> = {
        AUTOMATION_SEND_ENABLED: true,
        AUTOMATION_DRY_RUN: false,
      };
      return values[key];
    });
    atlasRuntime.getDefinition.mockResolvedValue({
      automation: { id: 'auto_1' },
      version: { id: 'ver_1' },
      nodes: [
        {
          nodeKey: 'action_1',
          type: 'ACTION',
          label: 'Enviar e-mail',
          positionX: 0,
          positionY: 0,
          config: { channel: 'EMAIL', body: 'Olá' },
        },
      ],
      edges: [],
    });

    await processor().processJob({
      runId: 'run_1',
      automationId: 'auto_1',
      versionId: 'ver_1',
      nodeKey: 'action_1',
      subject: {},
      payload: {},
      dryRun: false,
      attempt: 1,
    });

    expect(atlasRuntime.sendMessage).not.toHaveBeenCalled();
    expect(queueService.enqueue).not.toHaveBeenCalled();
    const missingEvent = reportRuntimeEvents().find(
      (event) =>
        (event.log as Record<string, unknown> | undefined)?.errorCode ===
        'MISSING_RECIPIENT',
    );
    expect(missingEvent).toMatchObject({
      runStatus: 'FAILED',
      log: {
        event: 'message.error',
        status: 'ERROR',
        errorCode: 'MISSING_RECIPIENT',
      },
    });
    const successEvent = reportRuntimeEvents().find(
      (event) => event.runStatus === 'SUCCESS',
    );
    expect(successEvent).toBeUndefined();
  });

  it('envio em dry-run sem destinatário não falha por MISSING_RECIPIENT', async () => {
    atlasRuntime.getDefinition.mockResolvedValue({
      automation: { id: 'auto_1' },
      version: { id: 'ver_1' },
      nodes: [
        {
          nodeKey: 'action_1',
          type: 'ACTION',
          label: 'Enviar e-mail',
          positionX: 0,
          positionY: 0,
          config: { channel: 'EMAIL', body: 'Olá' },
        },
      ],
      edges: [],
    });

    await processor().processJob({
      runId: 'run_1',
      automationId: 'auto_1',
      versionId: 'ver_1',
      nodeKey: 'action_1',
      subject: {},
      payload: {},
      dryRun: true,
      attempt: 1,
    });

    const missingEvent = reportRuntimeEvents().find(
      (event) =>
        (event.log as Record<string, unknown> | undefined)?.errorCode ===
        'MISSING_RECIPIENT',
    );
    expect(missingEvent).toBeUndefined();
    const sentEvent = reportRuntimeEvents().find(
      (event) =>
        (event.log as Record<string, unknown> | undefined)?.event ===
        'message.sent',
    );
    expect(sentEvent).toBeDefined();
  });

  it('envio real de WHATSAPP usa payload.telefone como destinatário', async () => {
    configGetMock.mockImplementation((key: string) => {
      const values: Record<string, unknown> = {
        AUTOMATION_SEND_ENABLED: true,
        AUTOMATION_DRY_RUN: false,
      };
      return values[key];
    });
    atlasRuntime.getDefinition.mockResolvedValue({
      automation: { id: 'auto_1' },
      version: { id: 'ver_1' },
      nodes: [
        {
          nodeKey: 'action_1',
          type: 'ACTION',
          label: 'Enviar WhatsApp',
          positionX: 0,
          positionY: 0,
          config: { channel: 'WHATSAPP', body: 'Olá', foo: 'bar' },
        },
      ],
      edges: [],
    });

    await processor().processJob({
      runId: 'run_1',
      automationId: 'auto_1',
      versionId: 'ver_1',
      nodeKey: 'action_1',
      subject: {},
      payload: { telefone: '+5511999999999' },
      dryRun: false,
      attempt: 1,
    });

    expect(atlasRuntime.sendMessage).toHaveBeenCalledWith(
      expect.objectContaining({ to: '+5511999999999' }),
    );
    const sentArg = atlasRuntime.sendMessage.mock.calls.at(-1)?.[0] as {
      meta: Record<string, unknown>;
    };
    expect(sentArg.meta.emailTracking).toBeUndefined();
    expect(sentArg.meta).not.toHaveProperty('foo');
    expect(sentArg.meta).not.toHaveProperty('body');
    expect(sentArg.meta).not.toHaveProperty('channel');
  });

  it('envio real de EMAIL só repassa meta da allowlist + emailTracking', async () => {
    configGetMock.mockImplementation((key: string) => {
      const values: Record<string, unknown> = {
        AUTOMATION_SEND_ENABLED: true,
        AUTOMATION_DRY_RUN: false,
      };
      return values[key];
    });
    atlasRuntime.getDefinition.mockResolvedValue({
      automation: { id: 'auto_1' },
      version: { id: 'ver_1' },
      nodes: [
        {
          nodeKey: 'action_1',
          type: 'ACTION',
          label: 'Enviar e-mail',
          positionX: 0,
          positionY: 0,
          config: {
            channel: 'EMAIL',
            body: 'Olá',
            subject: 'Assunto',
            trackEmail: false,
            foo: 'bar',
          },
        },
      ],
      edges: [],
    });

    await processor().processJob({
      runId: 'run_1',
      automationId: 'auto_1',
      versionId: 'ver_1',
      nodeKey: 'action_1',
      subject: {},
      payload: { email: 'ana@planfi.com.br' },
      dryRun: false,
      attempt: 1,
    });

    const sentArg = atlasRuntime.sendMessage.mock.calls.at(-1)?.[0] as {
      meta: Record<string, unknown>;
    };
    expect(sentArg.meta).toHaveProperty('emailTracking');
    expect(sentArg.meta.trackEmail).toBe(false);
    expect(sentArg.meta).not.toHaveProperty('foo');
    expect(sentArg.meta).not.toHaveProperty('body');
    expect(sentArg.meta).not.toHaveProperty('channel');
    expect(sentArg.meta).not.toHaveProperty('subject');
  });

  function lastReportRuntimeEvent(): Record<string, unknown> {
    const calls = reportRuntimeEvents().map((event) => [event]);
    const lastCall = calls.at(-1);

    if (!lastCall) {
      throw new Error('reportRuntimeEvent não foi chamado');
    }

    return lastCall[0];
  }

  function reportRuntimeEvents(): Array<Record<string, unknown>> {
    const calls = atlasRuntime.reportRuntimeEvent.mock
      .calls as unknown as Array<[Record<string, unknown>]>;
    return calls.map(([event]) => event);
  }
});
