import { InternalServerErrorException } from '@nestjs/common';
import { type CacheService } from '@modules/shared/infrastructure/cache.service';
import { type PrismaService } from '@modules/shared';
import { GetMarketSnapshotHandler } from '../queries/get-market-snapshot/get-market-snapshot.handler';
import { GetMarketSnapshotQuery } from '../queries/get-market-snapshot/get-market-snapshot.query';

describe('GetMarketSnapshotHandler', () => {
  let handler: GetMarketSnapshotHandler;
  let mockPrisma: Record<string, any>;
  let mockCache: { getOrSet: ReturnType<typeof vi.fn> };

  beforeEach(() => {
    mockPrisma = {
      listing: {
        aggregate: vi.fn(),
        count: vi.fn(),
      },
      $queryRaw: vi.fn(),
    };
    mockCache = {
      getOrSet: vi.fn((_key: string, loader: () => Promise<unknown>) => loader()),
    };
    const mockLogger = {
      log: vi.fn(),
      error: vi.fn(),
      warn: vi.fn(),
    };
    handler = new GetMarketSnapshotHandler(
      mockPrisma as unknown as PrismaService,
      mockCache as unknown as CacheService,
      mockLogger as any,
    );
  });

  it('returns market snapshot with all fields', async () => {
    mockPrisma.listing.aggregate.mockResolvedValue({
      _count: 12345,
      _avg: { priceVND: 4500000000n, pricePerM2: 65000000 },
    });
    mockPrisma.$queryRaw
      .mockResolvedValueOnce([{ median: 3800000000n }])     // median
      .mockResolvedValueOnce([{ avg_days: 42.3 }])          // days on market
      .mockResolvedValueOnce([{ avg_price: 4400000000 }])   // 1d ago avg
      .mockResolvedValueOnce([{ avg_price: 4550000000 }])   // 7d ago avg
      .mockResolvedValueOnce([{ avg_price: 4380000000 }]);  // 30d ago avg
    mockPrisma.listing.count.mockResolvedValue(178);

    const query = new GetMarketSnapshotQuery('HCMC', 'APARTMENT');
    const result = await handler.execute(query);

    expect(result.city).toBe('HCMC');
    expect(result.propertyType).toBe('APARTMENT');
    expect(result.activeCount).toBe(12345);
    expect(result.avgPrice).toBe(4500000000);
    expect(result.medianPrice).toBe(3800000000);
    expect(result.avgPricePerM2).toBe(65000000);
    expect(result.daysOnMarket).toBe(42);
    expect(result.newListings24h).toBe(178);
    expect(result.priceChangePct).toBeDefined();
    expect(typeof result.priceChangePct.d1).toBe('number');
    expect(typeof result.priceChangePct.d7).toBe('number');
    expect(typeof result.priceChangePct.d30).toBe('number');
  });

  it('returns snapshot without propertyType filter', async () => {
    mockPrisma.listing.aggregate.mockResolvedValue({
      _count: 500,
      _avg: { priceVND: 3000000000n, pricePerM2: 50000000 },
    });
    mockPrisma.$queryRaw
      .mockResolvedValueOnce([{ median: 2500000000n }])
      .mockResolvedValueOnce([{ avg_days: 30 }])
      .mockResolvedValueOnce([{ avg_price: 2900000000 }])
      .mockResolvedValueOnce([{ avg_price: 3100000000 }])
      .mockResolvedValueOnce([{ avg_price: 2800000000 }]);
    mockPrisma.listing.count.mockResolvedValue(50);

    const query = new GetMarketSnapshotQuery('HCMC');
    const result = await handler.execute(query);

    expect(result.city).toBe('HCMC');
    expect(result.propertyType).toBeUndefined();
    expect(result.activeCount).toBe(500);
  });

  it('handles empty data gracefully', async () => {
    mockPrisma.listing.aggregate.mockResolvedValue({
      _count: 0,
      _avg: { priceVND: null, pricePerM2: null },
    });
    mockPrisma.$queryRaw
      .mockResolvedValueOnce([{ median: null }])
      .mockResolvedValueOnce([{ avg_days: null }])
      .mockResolvedValueOnce([{ avg_price: null }])
      .mockResolvedValueOnce([{ avg_price: null }])
      .mockResolvedValueOnce([{ avg_price: null }]);
    mockPrisma.listing.count.mockResolvedValue(0);

    const query = new GetMarketSnapshotQuery('Hà Nội');
    const result = await handler.execute(query);

    expect(result.activeCount).toBe(0);
    expect(result.avgPrice).toBe(0);
    expect(result.medianPrice).toBe(0);
    expect(result.avgPricePerM2).toBe(0);
    expect(result.daysOnMarket).toBe(0);
    expect(result.newListings24h).toBe(0);
    expect(result.priceChangePct).toEqual({ d1: 0, d7: 0, d30: 0 });
  });

  it('uses cache with correct key', async () => {
    mockPrisma.listing.aggregate.mockResolvedValue({
      _count: 1,
      _avg: { priceVND: 1000000000n, pricePerM2: 50000000 },
    });
    mockPrisma.$queryRaw.mockResolvedValue([{ median: null, avg_days: null, avg_price: null }]);
    mockPrisma.listing.count.mockResolvedValue(0);

    const query = new GetMarketSnapshotQuery('HCMC', 'APARTMENT');
    await handler.execute(query);

    expect(mockCache.getOrSet).toHaveBeenCalledWith(
      expect.stringContaining('market_snapshot'),
      expect.any(Function),
      300,
      'market_snapshot',
    );
  });

  it('throws InternalServerErrorException on unexpected error', async () => {
    mockCache.getOrSet.mockRejectedValue(new Error('DB down'));

    const query = new GetMarketSnapshotQuery('HCMC');
    await expect(handler.execute(query)).rejects.toThrow(InternalServerErrorException);
  });
});
