import { ListingFeaturedExpiredEvent } from '../../domain/events/listing-featured-expired.event';
import { FeaturedListingExpiryCronService } from '../../infrastructure/cron/featured-listing-expiry-cron.service';

describe('FeaturedListingExpiryCronService', () => {
  let service: FeaturedListingExpiryCronService;
  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 FeaturedListingExpiryCronService(
      mockPrisma as any,
      mockEventBus as any,
      mockLogger as any,
    );
  });

  it('publishes ListingFeaturedExpiredEvent for each expired listing and logs the count', async () => {
    mockPrisma.$queryRaw.mockResolvedValue([{ id: 'listing-a' }, { id: 'listing-b' }]);

    await service.expireFeaturedListings();

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

    const events = mockEventBus.publish.mock.calls.map((c) => c[0]) as ListingFeaturedExpiredEvent[];
    expect(events[0]).toBeInstanceOf(ListingFeaturedExpiredEvent);
    expect(events.map((e) => e.aggregateId).sort()).toEqual(['listing-a', 'listing-b']);
    expect(events[0].eventName).toBe('listing.featured_expired');
    expect(events[0].expiredAt).toBeInstanceOf(Date);

    expect(mockLogger.log).toHaveBeenCalledWith(
      expect.stringContaining('Expired 2 featured listing(s)'),
      'FeaturedListingExpiryCronService',
    );
  });

  it('is a no-op when no rows match the predicate (idempotent across runs)', async () => {
    mockPrisma.$queryRaw.mockResolvedValue([]);

    await service.expireFeaturedListings();

    expect(mockEventBus.publish).not.toHaveBeenCalled();
    expect(mockLogger.log).not.toHaveBeenCalled();
    expect(mockLogger.debug).toHaveBeenCalledWith(
      'No featured listings to expire',
      'FeaturedListingExpiryCronService',
    );
  });

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

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

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

  it('uses a single atomic UPDATE ... RETURNING so concurrent runs do not double-update', async () => {
    // Simulate two parallel runs against the same DB; only the first sees the row.
    mockPrisma.$queryRaw
      .mockResolvedValueOnce([{ id: 'listing-1' }])
      .mockResolvedValueOnce([]);

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

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