import { Logger } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
import { NEVER, Observable, throwError } from 'rxjs';

import { isBrokerConnected, publishConfirmed } from './broker.util';

/**
 * `ClientProxy.emit()` returns a cold Observable that, for RMQ, completes without
 * emitting once `dispatchEvent` settles — and rejects if the publish fails. These
 * fakes reproduce each of those three shapes.
 */
function clientEmitting(source: Observable<unknown>): ClientProxy {
  return { emit: jest.fn().mockReturnValue(source) } as unknown as ClientProxy;
}

describe('publishConfirmed', () => {
  const logger = { error: jest.fn() } as unknown as Logger;

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

  it('is true when the publish completes (broker confirmed)', async () => {
    // Completes with no value — the success shape, and the case that throws
    // EmptyError without a defaultIfEmpty.
    const client = clientEmitting(new Observable<never>((s) => s.complete()));
    await expect(publishConfirmed(client, 'send-sms', {}, logger)).resolves.toBe(true);
  });

  it('is false when the publish never settles, rather than hanging', async () => {
    // A broker that is down: amqp-connection-manager buffers the publish and the
    // promise would never resolve. The timeout is what stops one dead broker from
    // hanging every caller of sendSms/sendEmail.
    const client = clientEmitting(NEVER);
    await expect(publishConfirmed(client, 'send-sms', {}, logger, 20)).resolves.toBe(
      false,
    );
    expect(logger.error).toHaveBeenCalled();
  });

  it('is false when the publish errors', async () => {
    const client = clientEmitting(throwError(() => new Error('channel closed')));
    await expect(publishConfirmed(client, 'send-email', {}, logger)).resolves.toBe(
      false,
    );
    expect(logger.error).toHaveBeenCalled();
  });
});

describe('isBrokerConnected', () => {
  /** Stands in for `ClientProxy.unwrap()`, which returns the AmqpConnectionManager. */
  function clientUnwrapping(manager: unknown): ClientProxy {
    return { unwrap: () => manager } as unknown as ClientProxy;
  }

  it('reports the connection manager state', () => {
    expect(isBrokerConnected(clientUnwrapping({ isConnected: () => true }))).toBe(
      true,
    );
    expect(isBrokerConnected(clientUnwrapping({ isConnected: () => false }))).toBe(
      false,
    );
  });

  it('is false when unwrap throws — the client never connected', () => {
    // ClientRMQ.unwrap() throws "Not initialized" while its internal client is
    // null, which is what a failed boot-time connect leaves behind. That is a
    // real down signal and must not be softened to "unknown".
    const uninitialised = {
      unwrap: () => {
        throw new Error('Not initialized. Please call the "connect" method first.');
      },
    } as unknown as ClientProxy;
    expect(isBrokerConnected(uninitialised)).toBe(false);
  });

  it('is null — not a guess — when the manager lacks isConnected or it throws', () => {
    // Guards the health endpoint against reporting "ok" if amqp-connection-manager
    // or Nest changes shape and the accessor we rely on disappears.
    expect(isBrokerConnected(clientUnwrapping(null))).toBeNull();
    expect(isBrokerConnected(clientUnwrapping({}))).toBeNull();
    expect(
      isBrokerConnected(
        clientUnwrapping({
          isConnected: () => {
            throw new Error('boom');
          },
        }),
      ),
    ).toBeNull();
  });
});
