import { type CacheService, type LoggerService } from '@modules/shared';
import { GetPriceMoversHandler } from '../queries/get-price-movers/get-price-movers.handler';
import { GetPriceMoversQuery } from '../queries/get-price-movers/get-price-movers.query';

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

  beforeEach(() => {
    mockPrisma = {
      $queryRaw: vi.fn(),
    };
    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 GetPriceMoversHandler(
      mockPrisma as any,
      mockCache as CacheService,
      mockLogger as LoggerService,
    );
  });

  it('returns top price gainers sorted by changePct descending', async () => {
    mockPrisma.$queryRaw.mockResolvedValue([
      { district: 'Quận 1', current_avg: 5_000_000_000, previous_avg: 4_000_000_000, sample_size: BigInt(15) },
      { district: 'Quận 7', current_avg: 3_000_000_000, previous_avg: 2_500_000_000, sample_size: BigInt(20) },
      { district: 'Bình Thạnh', current_avg: 2_000_000_000, previous_avg: 2_200_000_000, sample_size: BigInt(12) },
    ]);

    const query = new GetPriceMoversQuery('up', '7d', 5, 'district');
    const result = await handler.execute(query);

    expect(result.direction).toBe('up');
    expect(result.period).toBe('7d');
    expect(result.movers.length).toBe(2); // Only positive changes
    // Quận 1: +25%, Quận 7: +20%
    expect(result.movers[0].districtId).toBe('Quận 1');
    expect(result.movers[0].changePct).toBe(25);
    expect(result.movers[1].districtId).toBe('Quận 7');
    expect(result.movers[1].changePct).toBe(20);
  });

  it('returns top price losers sorted by changePct ascending', async () => {
    mockPrisma.$queryRaw.mockResolvedValue([
      { district: 'Quận 1', current_avg: 5_000_000_000, previous_avg: 4_000_000_000, sample_size: BigInt(15) },
      { district: 'Bình Thạnh', current_avg: 2_000_000_000, previous_avg: 2_200_000_000, sample_size: BigInt(12) },
      { district: 'Thủ Đức', current_avg: 1_800_000_000, previous_avg: 2_100_000_000, sample_size: BigInt(18) },
    ]);

    const query = new GetPriceMoversQuery('down', '7d', 5, 'district');
    const result = await handler.execute(query);

    expect(result.direction).toBe('down');
    expect(result.movers.length).toBe(2); // Only negative changes
    // Thủ Đức: -14.29%, Bình Thạnh: -9.09%
    expect(result.movers[0].districtId).toBe('Thủ Đức');
    expect(result.movers[1].districtId).toBe('Bình Thạnh');
    expect(result.movers[0].changePct).toBeLessThan(result.movers[1].changePct);
  });

  it('respects the limit parameter', async () => {
    mockPrisma.$queryRaw.mockResolvedValue([
      { district: 'A', current_avg: 200, previous_avg: 100, sample_size: BigInt(10) },
      { district: 'B', current_avg: 180, previous_avg: 100, sample_size: BigInt(10) },
      { district: 'C', current_avg: 160, previous_avg: 100, sample_size: BigInt(10) },
    ]);

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

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

  it('returns empty movers when no data', async () => {
    mockPrisma.$queryRaw.mockResolvedValue([]);

    const query = new GetPriceMoversQuery('up', '7d', 5, 'district');
    const result = await handler.execute(query);

    expect(result.movers).toEqual([]);
  });

  it('rounds changePct to two decimal places', async () => {
    mockPrisma.$queryRaw.mockResolvedValue([
      { district: 'Quận 1', current_avg: 3_333_333, previous_avg: 3_000_000, sample_size: BigInt(15) },
    ]);

    const query = new GetPriceMoversQuery('up', '7d', 5, 'district');
    const result = await handler.execute(query);

    expect(result.movers[0].changePct).toBe(11.11);
  });

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

    const query = new GetPriceMoversQuery('up', '7d', 5, 'district');
    await expect(handler.execute(query)).rejects.toThrow(
      'Không thể truy vấn biến động giá. Vui lòng thử lại sau.',
    );
  });
});
