import { ListingExpiringEvent } from '../../domain/events/listing-expiring.event';
import { ListingExpiryCronService } from '../../infrastructure/cron/listing-expiry-cron.service';

describe('ListingExpiryCronService', () => {
  let service: ListingExpiryCronService;
  let mockPrisma: { $queryRaw: ReturnType<typeof vi.fn> };
  let mockEventBus: { publish: ReturnType<typeof vi.fn> };
  let mockLogger: {
    log: ReturnType<typeof vi.fn>;
    debug: ReturnType<typeof vi.fn>;
    error: ReturnType<typeof vi.fn>;
  };

  beforeEach(() => {
    mockPrisma = { $queryRaw: vi.fn() };
    mockEventBus = { publish: vi.fn() };
    mockLogger = { log: vi.fn(), debug: vi.fn(), error: vi.fn() };

    service = new ListingExpiryCronService(
      mockPrisma as any,
      mockEventBus as any,
      mockLogger as any,
    );
  });

  it('publishes ListingExpiringEvent for each expiring listing and logs the count', async () => {
    const expiresAt = new Date(Date.now() + 2 * 24 * 60 * 60 * 1000);
    mockPrisma.$queryRaw.mockResolvedValue([
      { id: 'listing-a', sellerId: 'seller-a', expiresAt },
      { id: 'listing-b', sellerId: 'seller-b', expiresAt },
    ]);

    await service.notifyExpiringListings();

    expect(mockPrisma.$queryRaw).toHaveBeenCalledTimes(1);
    expect(mockEventBus.publish).toHaveBeenCalledTimes(2);

    const events = mockEventBus.publish.mock.calls.map((c) => c[0]) as ListingExpiringEvent[];
    expect(events[0]).toBeInstanceOf(ListingExpiringEvent);
    expect(events[1]).toBeInstanceOf(ListingExpiringEvent);
    expect(events.map((e) => e.aggregateId).sort()).toEqual(['listing-a', 'listing-b']);
    expect(events[0].eventName).toBe('listing.expiring');
    expect(events[0].sellerId).toBe('seller-a');
    expect(events[0].expiresAt).toBe(expiresAt);

    expect(mockLogger.log).toHaveBeenCalledWith(
      expect.stringContaining('2 listing(s)'),
      'ListingExpiryCronService',
    );
  });

  it('is a no-op when no listings are expiring (idempotent across runs)', async () => {
    mockPrisma.$queryRaw.mockResolvedValue([]);

    await service.notifyExpiringListings();

    expect(mockEventBus.publish).not.toHaveBeenCalled();
    expect(mockLogger.log).not.toHaveBeenCalled();
    expect(mockLogger.debug).toHaveBeenCalledWith(
      'No listings expiring in the next 3 days — nothing to notify',
      'ListingExpiryCronService',
    );
  });

  it('catches DB errors and logs them without throwing', async () => {
    const boom = new Error('connection lost');
    mockPrisma.$queryRaw.mockRejectedValue(boom);

    await expect(service.notifyExpiringListings()).resolves.toBeUndefined();

    expect(mockEventBus.publish).not.toHaveBeenCalled();
    expect(mockLogger.error).toHaveBeenCalledWith(
      expect.stringContaining('connection lost'),
      expect.any(String),
      'ListingExpiryCronService',
    );
  });

  it('uses a single atomic UPDATE ... RETURNING so concurrent runs do not double-notify', async () => {
    const expiresAt = new Date(Date.now() + 2 * 24 * 60 * 60 * 1000);
    // Simulate two parallel runs against the same DB; only the first sees the row.
    mockPrisma.$queryRaw
      .mockResolvedValueOnce([{ id: 'listing-1', sellerId: 'seller-1', expiresAt }])
      .mockResolvedValueOnce([]);

    await Promise.all([
      service.notifyExpiringListings(),
      service.notifyExpiringListings(),
    ]);

    // Only one event published across both runs.
    expect(mockEventBus.publish).toHaveBeenCalledTimes(1);
    expect(mockPrisma.$queryRaw).toHaveBeenCalledTimes(2);
  });

  it('publishes events with correct sellerId and expiresAt per row', async () => {
    const expiresAtA = new Date(Date.now() + 1 * 24 * 60 * 60 * 1000);
    const expiresAtB = new Date(Date.now() + 2.5 * 24 * 60 * 60 * 1000);
    mockPrisma.$queryRaw.mockResolvedValue([
      { id: 'listing-a', sellerId: 'seller-x', expiresAt: expiresAtA },
      { id: 'listing-b', sellerId: 'seller-y', expiresAt: expiresAtB },
    ]);

    await service.notifyExpiringListings();

    const events = mockEventBus.publish.mock.calls.map((c) => c[0]) as ListingExpiringEvent[];
    expect(events[0].aggregateId).toBe('listing-a');
    expect(events[0].sellerId).toBe('seller-x');
    expect(events[0].expiresAt).toBe(expiresAtA);

    expect(events[1].aggregateId).toBe('listing-b');
    expect(events[1].sellerId).toBe('seller-y');
    expect(events[1].expiresAt).toBe(expiresAtB);
  });
});
