import { type CacheService, type LoggerService } from '@modules/shared';
import { GetTrendingAreasHandler } from '../queries/get-trending-areas/get-trending-areas.handler';
import { GetTrendingAreasQuery } from '../queries/get-trending-areas/get-trending-areas.query';

describe('GetTrendingAreasHandler', () => {
  let handler: GetTrendingAreasHandler;
  let mockPrisma: { $queryRaw: ReturnType<typeof vi.fn>; marketIndex: { findMany: ReturnType<typeof vi.fn> } };
  let mockCache: Partial<CacheService>;
  let mockLogger: Partial<LoggerService>;

  beforeEach(() => {
    mockPrisma = {
      $queryRaw: vi.fn(),
      marketIndex: {
        findMany: vi.fn(),
      },
    };
    // Bypass @Cacheable decorator by making CacheService.getOrSet call the loader directly
    mockCache = {
      getOrSet: vi.fn((_key: string, loader: () => Promise<unknown>) => loader()),
    } as unknown as Partial<CacheService>;
    mockLogger = { error: vi.fn(), warn: vi.fn(), log: vi.fn() } as unknown as Partial<LoggerService>;

    handler = new GetTrendingAreasHandler(
      mockPrisma as any,
      mockCache as CacheService,
      mockLogger as LoggerService,
    );
  });

  it('returns top trending districts sorted by score', async () => {
    mockPrisma.$queryRaw.mockResolvedValue([
      { district: 'Quận 1', new_listings: BigInt(10), inquiries: BigInt(50), views: BigInt(200) },
      { district: 'Quận 7', new_listings: BigInt(20), inquiries: BigInt(30), views: BigInt(400) },
      { district: 'Bình Thạnh', new_listings: BigInt(5), inquiries: BigInt(5), views: BigInt(50) },
    ]);
    mockPrisma.marketIndex.findMany.mockResolvedValue([
      { district: 'Quận 1', yoyChange: 0.12 },
      { district: 'Quận 7', yoyChange: 0.05 },
    ]);

    const query = new GetTrendingAreasQuery(7, 10, 'district');
    const result = await handler.execute(query);

    expect(result.period).toBe(7);
    expect(result.level).toBe('district');
    expect(result.areas.length).toBe(3);

    // Quận 1 score = 50*0.6 + 200*0.3 + 10*0.1 = 30 + 60 + 1 = 91
    // Quận 7 score = 30*0.6 + 400*0.3 + 20*0.1 = 18 + 120 + 2 = 140
    // Bình Thạnh score = 5*0.6 + 50*0.3 + 5*0.1 = 3 + 15 + 0.5 = 18.5
    // Expected order: Quận 7 (1st), Quận 1 (2nd), Bình Thạnh (3rd)
    expect(result.areas[0].districtId).toBe('Quận 7');
    expect(result.areas[0].scoreRank).toBe(1);
    expect(result.areas[1].districtId).toBe('Quận 1');
    expect(result.areas[2].districtId).toBe('Bình Thạnh');
  });

  it('respects the limit parameter', async () => {
    mockPrisma.$queryRaw.mockResolvedValue([
      { district: 'A', new_listings: BigInt(1), inquiries: BigInt(10), views: BigInt(100) },
      { district: 'B', new_listings: BigInt(1), inquiries: BigInt(8), views: BigInt(80) },
      { district: 'C', new_listings: BigInt(1), inquiries: BigInt(6), views: BigInt(60) },
    ]);
    mockPrisma.marketIndex.findMany.mockResolvedValue([]);

    const query = new GetTrendingAreasQuery(7, 2, 'district');
    const result = await handler.execute(query);

    expect(result.areas.length).toBe(2);
    expect(result.limit).toBe(2);
  });

  it('returns empty areas when no active listings in window', async () => {
    mockPrisma.$queryRaw.mockResolvedValue([]);
    mockPrisma.marketIndex.findMany.mockResolvedValue([]);

    const query = new GetTrendingAreasQuery(7, 10, 'district');
    const result = await handler.execute(query);

    expect(result.areas).toEqual([]);
    expect(mockPrisma.marketIndex.findMany).not.toHaveBeenCalled();
  });

  it('attaches yoyChange from market index as priceChangePct', async () => {
    mockPrisma.$queryRaw.mockResolvedValue([
      { district: 'Quận 1', new_listings: BigInt(5), inquiries: BigInt(20), views: BigInt(100) },
    ]);
    mockPrisma.marketIndex.findMany.mockResolvedValue([
      { district: 'Quận 1', yoyChange: 0.08 },
    ]);

    const query = new GetTrendingAreasQuery(14, 10, 'district');
    const result = await handler.execute(query);

    expect(result.areas[0].priceChangePct).toBe(0.08);
  });

  it('sets priceChangePct to null when market index data is missing', async () => {
    mockPrisma.$queryRaw.mockResolvedValue([
      { district: 'Huyện Củ Chi', new_listings: BigInt(3), inquiries: BigInt(5), views: BigInt(40) },
    ]);
    mockPrisma.marketIndex.findMany.mockResolvedValue([]);

    const query = new GetTrendingAreasQuery(7, 10, 'district');
    const result = await handler.execute(query);

    expect(result.areas[0].priceChangePct).toBeNull();
  });

  it('throws InternalServerErrorException on unexpected errors', async () => {
    mockPrisma.$queryRaw.mockRejectedValue(new Error('DB connection lost'));

    const query = new GetTrendingAreasQuery(7, 10, 'district');
    await expect(handler.execute(query)).rejects.toThrow(
      'Không thể truy vấn khu vực xu hướng. Vui lòng thử lại sau.',
    );
  });
});
