import { UnauthorizedException } from '@nestjs/common';
import { AutomationsService } from './automations.service';

describe('AutomationsService', () => {
  const eventModel = {
    create: jest.fn(),
    findOne: jest.fn(),
    updateOne: jest.fn(),
  };
  const atlasRuntime = {
    findPublishedByTrigger: jest.fn(),
    createRun: jest.fn(),
    reportRuntimeEvent: jest.fn(),
    reportIngestionEvent: jest.fn(),
  };
  const queueService = {
    enqueue: jest.fn(),
  };
  const configService = {
    get: jest.fn((key: string) => {
      const values: Record<string, unknown> = {
        AUTOMATION_SEND_ENABLED: false,
        AUTOMATION_DRY_RUN: true,
      };
      return values[key];
    }),
  };

  beforeEach(() => {
    jest.clearAllMocks();
    configService.get.mockImplementation((key: string) => {
      const values: Record<string, unknown> = {
        AUTOMATION_SEND_ENABLED: false,
        AUTOMATION_DRY_RUN: true,
      };
      return values[key];
    });
    eventModel.findOne.mockResolvedValue(null);
    atlasRuntime.reportRuntimeEvent.mockResolvedValue({});
    atlasRuntime.reportIngestionEvent.mockResolvedValue({});
  });

  function service() {
    return new AutomationsService(
      eventModel as never,
      atlasRuntime as never,
      queueService as never,
      configService as never,
    );
  }

  it('ingere evento novo, cria run no Atlas e enfileira job inicial', async () => {
    eventModel.create.mockResolvedValue({
      eventName: 'payment.failed',
      source: 'stripe',
      subjectType: 'contact',
      subjectId: 'contact_1',
      contactId: 'contact_1',
      payload: { nome: 'Ana', email: 'ana@planfi.com.br' },
      idempotencyKey: 'evt_1',
    });
    atlasRuntime.findPublishedByTrigger.mockResolvedValue([
      { id: 'auto_1', currentVersionId: 'ver_1', triggerNodeKey: 'trigger_1' },
    ]);
    atlasRuntime.createRun.mockResolvedValue({ id: 'run_1' });
    queueService.enqueue.mockResolvedValue({ id: 'job_1', degraded: false });

    const result = await service().recordAndIngest({
      eventName: 'payment.failed',
      source: 'stripe',
      contactId: 'contact_1',
      payload: { nome: 'Ana', email: 'ana@planfi.com.br' },
      idempotencyKey: 'evt_1',
    });

    expect(result).toEqual({
      accepted: true,
      duplicate: false,
      matched: 1,
      enqueued: 1,
      degraded: false,
    });
    expect(atlasRuntime.createRun).toHaveBeenCalledWith(
      expect.objectContaining({
        automationId: 'auto_1',
        sourceEventId: 'evt_1',
        dryRun: true,
      }),
    );
    expect(queueService.enqueue).toHaveBeenCalledWith(
      expect.objectContaining({
        runId: 'run_1',
        automationId: 'auto_1',
        versionId: 'ver_1',
        nodeKey: 'trigger_1',
      }),
    );
    expect(atlasRuntime.reportIngestionEvent).toHaveBeenCalledWith(
      expect.objectContaining({
        eventName: 'payment.failed',
        idempotencyKey: 'evt_1',
        status: 'ENQUEUED',
        matched: 1,
        enqueued: 1,
        degraded: false,
      }),
    );
  });

  it('cria run não-dry-run quando as duas flags globais habilitam envio real', async () => {
    configService.get.mockImplementation((key: string) => {
      const values: Record<string, unknown> = {
        AUTOMATION_SEND_ENABLED: 'true',
        AUTOMATION_DRY_RUN: 'false',
      };
      return values[key];
    });
    eventModel.create.mockResolvedValue({
      eventName: 'user.registered',
      source: 'atlas',
      userId: 'user_1',
      payload: { nome: 'Ana', email: 'ana@planfi.com.br' },
      idempotencyKey: 'evt_real',
    });
    atlasRuntime.findPublishedByTrigger.mockResolvedValue([
      { id: 'auto_1', currentVersionId: 'ver_1', triggerNodeKey: 'trigger_1' },
    ]);
    atlasRuntime.createRun.mockResolvedValue({ id: 'run_1' });
    queueService.enqueue.mockResolvedValue({ id: 'job_1', degraded: false });

    await service().recordAndIngest({
      eventName: 'user.registered',
      source: 'atlas',
      userId: 'user_1',
      payload: { nome: 'Ana', email: 'ana@planfi.com.br' },
      idempotencyKey: 'evt_real',
    });

    expect(atlasRuntime.createRun).toHaveBeenCalledWith(
      expect.objectContaining({ dryRun: false }),
    );
    expect(queueService.enqueue).toHaveBeenCalledWith(
      expect.objectContaining({ dryRun: false }),
    );
  });

  it('retorna erro estruturado e reporta falha de ingestão quando Mongo está indisponível', async () => {
    eventModel.create.mockRejectedValue({
      name: 'MongooseServerSelectionError',
      message: 'connect ECONNREFUSED 127.0.0.1:27017',
    });

    await expect(
      service().recordAndIngest({
        eventName: 'user.registered',
        source: 'atlas',
        userId: 'user_1',
        payload: { nome: 'Ana', email: 'ana@planfi.com.br' },
        idempotencyKey: 'user.registered:user_1',
      }),
    ).rejects.toMatchObject({
      response: {
        accepted: false,
        stage: 'event_store',
        errorCode: 'AUTOMATION_EVENT_STORE_UNAVAILABLE',
        retryable: true,
      },
    });

    expect(atlasRuntime.reportIngestionEvent).toHaveBeenCalledWith(
      expect.objectContaining({
        eventName: 'user.registered',
        idempotencyKey: 'user.registered:user_1',
        status: 'FAILED',
        errorCode: 'AUTOMATION_EVENT_STORE_UNAVAILABLE',
      }),
    );
    expect(atlasRuntime.findPublishedByTrigger).not.toHaveBeenCalled();
    expect(queueService.enqueue).not.toHaveBeenCalled();
  });

  it('retorna erro estruturado e reporta falha quando o Atlas nega a service key do BFF', async () => {
    eventModel.create.mockResolvedValue({
      eventName: 'user.registered',
      source: 'atlas',
      userId: 'user_1',
      payload: { nome: 'Ana', email: 'ana@planfi.com.br' },
      idempotencyKey: 'user.registered:user_1',
    });
    atlasRuntime.findPublishedByTrigger.mockRejectedValue(
      new UnauthorizedException('Service key negada: scope_missing'),
    );

    await expect(
      service().recordAndIngest({
        eventName: 'user.registered',
        source: 'atlas',
        userId: 'user_1',
        payload: { nome: 'Ana', email: 'ana@planfi.com.br' },
        idempotencyKey: 'user.registered:user_1',
      }),
    ).rejects.toMatchObject({
      response: {
        accepted: false,
        stage: 'atlas_runtime_authorization',
        errorCode: 'BFF_TO_ATLAS_AUTHORIZATION_FAILED',
        retryable: false,
      },
    });

    expect(atlasRuntime.reportIngestionEvent).toHaveBeenCalledWith(
      expect.objectContaining({
        eventName: 'user.registered',
        idempotencyKey: 'user.registered:user_1',
        status: 'FAILED',
        errorCode: 'BFF_TO_ATLAS_AUTHORIZATION_FAILED',
      }),
    );
    expect(queueService.enqueue).not.toHaveBeenCalled();
  });

  it('swallow duplicate key 11000 e não reingere evento duplicado', async () => {
    eventModel.create.mockRejectedValue({ code: 11000 });
    eventModel.findOne.mockResolvedValue({
      eventName: 'payment.failed',
      source: 'stripe',
      payload: {},
      idempotencyKey: 'evt_dup',
      processedAt: new Date('2026-01-01T00:00:00.000Z'),
    });

    const result = await service().recordAndIngest({
      eventName: 'payment.failed',
      source: 'stripe',
      payload: {},
      idempotencyKey: 'evt_dup',
    });

    expect(result).toEqual({
      accepted: true,
      duplicate: true,
      matched: 0,
      enqueued: 0,
      degraded: false,
    });
    expect(atlasRuntime.findPublishedByTrigger).not.toHaveBeenCalled();
    expect(queueService.enqueue).not.toHaveBeenCalled();
  });

  it('reingere duplicate key 11000 quando o evento anterior ainda não foi processado', async () => {
    eventModel.create.mockRejectedValue({ code: 11000 });
    eventModel.findOne.mockResolvedValue({
      eventName: 'payment.failed',
      source: 'stripe',
      subjectType: 'contact',
      subjectId: 'contact_1',
      contactId: 'contact_1',
      payload: { nome: 'Ana', email: 'ana@planfi.com.br' },
      idempotencyKey: 'evt_pending',
      processedAt: null,
    });
    atlasRuntime.findPublishedByTrigger.mockResolvedValue([
      { id: 'auto_1', currentVersionId: 'ver_1', triggerNodeKey: 'trigger_1' },
    ]);
    atlasRuntime.createRun.mockResolvedValue({ id: 'run_1' });
    queueService.enqueue.mockResolvedValue({ id: 'job_1', degraded: false });

    const result = await service().recordAndIngest({
      eventName: 'payment.failed',
      source: 'stripe',
      payload: {},
      idempotencyKey: 'evt_pending',
    });

    expect(result).toEqual({
      accepted: true,
      duplicate: true,
      matched: 1,
      enqueued: 1,
      degraded: false,
    });
    expect(atlasRuntime.findPublishedByTrigger).toHaveBeenCalledWith(
      'payment.failed',
    );
    expect(eventModel.updateOne).toHaveBeenCalledTimes(1);
    const updateCall = eventModel.updateOne.mock.calls[0] as [
      { idempotencyKey: string },
      { $set: { processedAt: unknown } },
    ];
    expect(updateCall[0]).toEqual({ idempotencyKey: 'evt_pending' });
    expect(updateCall[1].$set.processedAt).toBeInstanceOf(Date);
  });

  it('marca run como falho quando a fila está em modo degradado', async () => {
    eventModel.create.mockResolvedValue({
      eventName: 'payment.failed',
      source: 'stripe',
      contactId: 'contact_1',
      payload: { nome: 'Ana', email: 'ana@planfi.com.br' },
      idempotencyKey: 'evt_degraded',
    });
    atlasRuntime.findPublishedByTrigger.mockResolvedValue([
      { id: 'auto_1', currentVersionId: 'ver_1', triggerNodeKey: 'trigger_1' },
    ]);
    atlasRuntime.createRun.mockResolvedValue({ id: 'run_1' });
    queueService.enqueue.mockResolvedValue({ id: null, degraded: true });

    const result = await service().recordAndIngest({
      eventName: 'payment.failed',
      source: 'stripe',
      contactId: 'contact_1',
      payload: { nome: 'Ana', email: 'ana@planfi.com.br' },
      idempotencyKey: 'evt_degraded',
    });

    expect(result).toMatchObject({
      accepted: true,
      degraded: true,
      enqueued: 0,
    });
    expect(atlasRuntime.reportRuntimeEvent).toHaveBeenCalledTimes(1);
    const runtimeReportCall = atlasRuntime.reportRuntimeEvent.mock.calls[0] as [
      {
        runId: string;
        runStatus: string;
        log: { event: string; status: string; errorCode: string };
      },
    ];
    expect(runtimeReportCall[0]).toMatchObject({
      runId: 'run_1',
      runStatus: 'FAILED',
      log: {
        event: 'queue.degraded',
        status: 'ERROR',
        errorCode: 'QUEUE_DEGRADED',
      },
    });
  });

  it('scheduleWait enfileira job resume-atlas com o delay informado', async () => {
    queueService.enqueue.mockResolvedValue({ id: 'job_w', degraded: false });

    const result = await service().scheduleWait({
      runId: 'run_9',
      delayMs: 600_000,
    });

    expect(queueService.enqueue).toHaveBeenCalledWith(
      expect.objectContaining({
        kind: 'resume-atlas',
        runId: 'run_9',
        automationId: 'run_9',
        nodeKey: 'resume-atlas',
      }),
      600_000,
    );
    expect(result).toEqual({ accepted: true, degraded: false });
  });

  // ARFIX-09: scheduleWait encaminha waitSeq para o payload do job
  it('scheduleWait encaminha waitSeq para o payload do job enfileirado', async () => {
    queueService.enqueue.mockResolvedValue({ id: 'job_w2', degraded: false });

    await service().scheduleWait({
      runId: 'run_wait_seq',
      delayMs: 30_000,
      waitKey: 'wait_node_1',
      waitSeq: 'wait-log-id-abc123',
    });

    expect(queueService.enqueue).toHaveBeenCalledWith(
      expect.objectContaining({
        kind: 'resume-atlas',
        runId: 'run_wait_seq',
        nodeKey: 'wait_node_1',
        waitSeq: 'wait-log-id-abc123',
      }),
      30_000,
    );
  });

  it('scheduleWait encaminha waitSeq undefined quando não fornecido (fallback no port)', async () => {
    queueService.enqueue.mockResolvedValue({ id: 'job_w3', degraded: false });

    await service().scheduleWait({
      runId: 'run_no_seq',
      delayMs: 0,
    });

    // waitSeq deve estar presente na chamada (como undefined), não ausente —
    // o port gerará um UUID quando receber undefined, garantindo não-colisão.
    const callPayload = queueService.enqueue.mock.calls[0][0] as Record<
      string,
      unknown
    >;
    expect(callPayload).toHaveProperty('waitSeq');
    expect(callPayload.waitSeq).toBeUndefined();
  });

  // ARFIX-10: idempotência de enqueue por automação no caminho de duplicate-key
  it('não re-enfileira automação quando createRun retorna o mesmo runId na re-tentativa', async () => {
    // Simula o caminho: create lança 11000, evento existe sem processedAt → ingest()
    // é chamado novamente. createRun retorna o MESMO runId (idempotente no Atlas).
    eventModel.create.mockRejectedValue({ code: 11000 });
    eventModel.findOne.mockResolvedValue({
      eventName: 'payment.failed',
      source: 'stripe',
      subjectType: 'contact',
      subjectId: 'contact_1',
      contactId: 'contact_1',
      payload: { nome: 'Ana' },
      idempotencyKey: 'evt_idem',
      processedAt: null,
    });
    atlasRuntime.findPublishedByTrigger.mockResolvedValue([
      { id: 'auto_1', currentVersionId: 'ver_1', triggerNodeKey: 'trigger_1' },
    ]);
    // createRun retorna SEMPRE o mesmo runId (idempotente por automationId+sourceEventId)
    atlasRuntime.createRun.mockResolvedValue({ id: 'run_idem' });
    queueService.enqueue.mockResolvedValue({ id: 'job_idem', degraded: false });

    const result = await service().recordAndIngest({
      eventName: 'payment.failed',
      source: 'stripe',
      payload: {},
      idempotencyKey: 'evt_idem',
    });

    // enqueue deve ser chamado apenas UMA vez mesmo que ingest() fosse chamado de
    // novo (o Set enqueuedRunIds bloqueia o segundo enfileiramento do mesmo runId).
    expect(queueService.enqueue).toHaveBeenCalledTimes(1);
    expect(result).toMatchObject({
      accepted: true,
      duplicate: true,
      matched: 1,
      enqueued: 1,
    });
  });

  // ARFIX-10: isolamento por iteração — falha de uma automação não aborta as demais
  it('falha de uma automação não aborta o processamento das demais automações', async () => {
    eventModel.create.mockResolvedValue({
      eventName: 'user.registered',
      source: 'atlas',
      userId: 'user_1',
      payload: {},
      idempotencyKey: 'evt_multi',
    });
    atlasRuntime.findPublishedByTrigger.mockResolvedValue([
      {
        id: 'auto_fail',
        currentVersionId: 'ver_1',
        triggerNodeKey: 'trigger_1',
      },
      { id: 'auto_ok', currentVersionId: 'ver_2', triggerNodeKey: 'trigger_1' },
    ]);
    // Primeira automação: createRun lança erro
    atlasRuntime.createRun
      .mockRejectedValueOnce(new Error('Atlas indisponível'))
      .mockResolvedValueOnce({ id: 'run_ok' });
    queueService.enqueue.mockResolvedValue({ id: 'job_ok', degraded: false });

    const result = await service().recordAndIngest({
      eventName: 'user.registered',
      source: 'atlas',
      userId: 'user_1',
      payload: {},
      idempotencyKey: 'evt_multi',
    });

    // Deve ter tentado as duas automações: enqueue chamado para a segunda
    expect(queueService.enqueue).toHaveBeenCalledTimes(1);
    expect(queueService.enqueue).toHaveBeenCalledWith(
      expect.objectContaining({ runId: 'run_ok', automationId: 'auto_ok' }),
    );
    // matched conta as duas automações; enqueued conta só a que funcionou
    expect(result).toMatchObject({
      accepted: true,
      matched: 2,
      enqueued: 1,
      degraded: true,
    });
  });
});
