import { ListingApprovedEvent } from '@modules/admin/domain/events/listing-approved.event';
import { InquiryReadEvent } from '@modules/inquiries/domain/events/inquiry-read.event';
import { ListingPriceChangedEvent } from '@modules/listings/domain/events/listing-price-changed.event';
import {
  ResidentialInquiryReplyListener,
  ResidentialNewListingInProjectListener,
  ResidentialPriceDropListener,
} from '../listeners/residential-events.listener';

function createMockPrisma() {
  return {
    listing: { findUnique: vi.fn() },
    savedSearch: { findMany: vi.fn().mockResolvedValue([]) },
  };
}

function createMockGateway() {
  return {
    emitResidentialEvent: vi.fn(),
  };
}

function createMockLogger() {
  return { log: vi.fn(), warn: vi.fn(), error: vi.fn() };
}

describe('ResidentialPriceDropListener', () => {
  let listener: ResidentialPriceDropListener;
  let prisma: ReturnType<typeof createMockPrisma>;
  let gateway: ReturnType<typeof createMockGateway>;
  let logger: ReturnType<typeof createMockLogger>;

  const listing = {
    id: 'listing-1',
    sellerId: 'seller-1',
    transactionType: 'SALE',
    priceVND: 2_000_000_000n,
    property: {
      title: 'Căn hộ 2PN Quận 7',
      propertyType: 'APARTMENT',
      areaM2: 70,
      bedrooms: 2,
      district: 'Quận 7',
      city: 'Hồ Chí Minh',
      projectDevelopmentId: null,
    },
  };

  beforeEach(() => {
    prisma = createMockPrisma();
    gateway = createMockGateway();
    logger = createMockLogger();
    listener = new ResidentialPriceDropListener(
      prisma as any,
      gateway as any,
      logger as any,
    );
  });

  it('emits residential:price-drop to each user with a matching saved search', async () => {
    prisma.listing.findUnique.mockResolvedValue(listing);
    prisma.savedSearch.findMany.mockResolvedValue([
      {
        id: 'ss-1',
        userId: 'user-1',
        name: 'Quận 7 căn hộ',
        filters: { city: 'Hồ Chí Minh', district: 'Quận 7', priceMax: 3_000_000_000 },
      },
      {
        id: 'ss-2',
        userId: 'user-2',
        name: 'Quận 1',
        filters: { district: 'Quận 1' },
      },
    ]);

    const event = new ListingPriceChangedEvent('listing-1', 2_500_000_000n, 2_000_000_000n);
    await listener.handle(event);

    expect(gateway.emitResidentialEvent).toHaveBeenCalledTimes(1);
    expect(gateway.emitResidentialEvent).toHaveBeenCalledWith(
      'user-1',
      'residential:price-drop',
      expect.objectContaining({
        listingId: 'listing-1',
        savedSearchId: 'ss-1',
        oldPrice: '2500000000',
        newPrice: '2000000000',
      }),
    );
  });

  it('does not emit when the new price is not lower than the old price', async () => {
    const event = new ListingPriceChangedEvent('listing-1', 1_000_000_000n, 1_200_000_000n);
    await listener.handle(event);

    expect(prisma.listing.findUnique).not.toHaveBeenCalled();
    expect(gateway.emitResidentialEvent).not.toHaveBeenCalled();
  });

  it('skips saved searches owned by the listing seller', async () => {
    prisma.listing.findUnique.mockResolvedValue(listing);
    prisma.savedSearch.findMany.mockResolvedValue([
      { id: 'ss-self', userId: 'seller-1', name: 'mine', filters: {} },
    ]);

    const event = new ListingPriceChangedEvent('listing-1', 2_500_000_000n, 2_000_000_000n);
    await listener.handle(event);

    expect(gateway.emitResidentialEvent).not.toHaveBeenCalled();
  });

  it('swallows infrastructure errors without throwing', async () => {
    prisma.listing.findUnique.mockRejectedValue(new Error('db down'));

    const event = new ListingPriceChangedEvent('listing-1', 2_000_000_000n, 1_000_000_000n);
    await expect(listener.handle(event)).resolves.not.toThrow();
    expect(logger.warn).toHaveBeenCalled();
  });
});

describe('ResidentialNewListingInProjectListener', () => {
  let listener: ResidentialNewListingInProjectListener;
  let prisma: ReturnType<typeof createMockPrisma>;
  let gateway: ReturnType<typeof createMockGateway>;
  let logger: ReturnType<typeof createMockLogger>;

  beforeEach(() => {
    prisma = createMockPrisma();
    gateway = createMockGateway();
    logger = createMockLogger();
    listener = new ResidentialNewListingInProjectListener(
      prisma as any,
      gateway as any,
      logger as any,
    );
  });

  it('emits residential:new-listing-in-project to users tracking the project', async () => {
    prisma.listing.findUnique.mockResolvedValue({
      id: 'listing-9',
      sellerId: 'seller-9',
      priceVND: 3_500_000_000n,
      property: {
        title: 'Vinhomes Grand Park S5.02',
        district: 'Quận 9',
        city: 'Hồ Chí Minh',
        projectDevelopmentId: 'project-vgp',
      },
    });
    prisma.savedSearch.findMany.mockResolvedValue([
      { id: 'ss-tracker', userId: 'user-10', name: 'VGP', filters: { projectId: 'project-vgp' } },
      { id: 'ss-other', userId: 'user-11', name: 'khác', filters: { projectId: 'project-other' } },
      { id: 'ss-no-project', userId: 'user-12', name: 'no-project', filters: {} },
    ]);

    const event = new ListingApprovedEvent('listing-9', 'admin-1');
    await listener.handle(event);

    expect(gateway.emitResidentialEvent).toHaveBeenCalledTimes(1);
    expect(gateway.emitResidentialEvent).toHaveBeenCalledWith(
      'user-10',
      'residential:new-listing-in-project',
      expect.objectContaining({
        listingId: 'listing-9',
        projectId: 'project-vgp',
        price: '3500000000',
      }),
    );
  });

  it('does not emit when the listing has no linked project', async () => {
    prisma.listing.findUnique.mockResolvedValue({
      id: 'listing-9',
      sellerId: 'seller-9',
      priceVND: 1n,
      property: { title: 't', district: 'd', city: 'c', projectDevelopmentId: null },
    });

    const event = new ListingApprovedEvent('listing-9', 'admin-1');
    await listener.handle(event);

    expect(prisma.savedSearch.findMany).not.toHaveBeenCalled();
    expect(gateway.emitResidentialEvent).not.toHaveBeenCalled();
  });
});

describe('ResidentialInquiryReplyListener', () => {
  let listener: ResidentialInquiryReplyListener;
  let gateway: ReturnType<typeof createMockGateway>;
  let logger: ReturnType<typeof createMockLogger>;

  beforeEach(() => {
    gateway = createMockGateway();
    logger = createMockLogger();
    listener = new ResidentialInquiryReplyListener(gateway as any, logger as any);
  });

  it('emits residential:inquiry-reply to the inquiry author', async () => {
    const event = new InquiryReadEvent('inq-1', 'listing-1', 'user-author');

    await listener.handle(event);

    expect(gateway.emitResidentialEvent).toHaveBeenCalledWith(
      'user-author',
      'residential:inquiry-reply',
      expect.objectContaining({
        inquiryId: 'inq-1',
        listingId: 'listing-1',
      }),
    );
  });

  it('swallows emission errors without throwing', async () => {
    gateway.emitResidentialEvent.mockImplementation(() => {
      throw new Error('server error');
    });
    const event = new InquiryReadEvent('inq-2', 'listing-2', 'user-2');

    await expect(listener.handle(event)).resolves.not.toThrow();
    expect(logger.warn).toHaveBeenCalled();
  });
});
