import { InternalServerErrorException } from '@nestjs/common';
import { type CacheService, type PrismaService } from '@modules/shared';
import { DomainException } from '@modules/shared';
import { type IAVMService, type ValuationResult } from '../../domain/services/avm-service';
import { ValuationComparisonHandler } from '../queries/valuation-comparison/valuation-comparison.handler';
import { ValuationComparisonQuery } from '../queries/valuation-comparison/valuation-comparison.query';

describe('ValuationComparisonHandler', () => {
  let handler: ValuationComparisonHandler;
  let mockAvm: { [K in keyof IAVMService]: ReturnType<typeof vi.fn> };
  let mockPrisma: { property: { findMany: ReturnType<typeof vi.fn> } };
  let mockLogger: { error: ReturnType<typeof vi.fn> };

  const makeValuation = (price: string, confidence: number, pricePerM2: number): ValuationResult => ({
    estimatedPrice: price,
    confidence,
    pricePerM2,
    comparables: [
      {
        propertyId: 'comp-1',
        address: '123 Test',
        district: 'Quận 1',
        priceVND: price,
        pricePerM2,
        areaM2: 70,
        propertyType: 'APARTMENT' as const,
        distanceMeters: 200,
        soldAt: '2026-03-01T00:00:00.000Z',
      },
    ],
    modelVersion: 'avm-v1.0',
  });

  const sampleProperties = [
    { id: 'prop-1', address: '10 Nguyễn Huệ', district: 'Quận 1', areaM2: 80, propertyType: 'APARTMENT' },
    { id: 'prop-2', address: '20 Lê Lợi', district: 'Quận 3', areaM2: 100, propertyType: 'HOUSE' },
  ];

  beforeEach(() => {
    mockAvm = {
      estimateValue: vi.fn(),
      getComparables: vi.fn(),
      estimateBatch: vi.fn(),
    };
    mockPrisma = {
      property: { findMany: vi.fn() },
    };
    mockLogger = { error: vi.fn() };
    const mockCache = {
      getOrSet: vi.fn((_key: string, loader: () => Promise<unknown>) => loader()),
    } as unknown as CacheService;
    handler = new ValuationComparisonHandler(
      mockAvm as any,
      mockPrisma as unknown as PrismaService,
      mockCache,
      mockLogger as any,
    );
  });

  it('compares valuations for multiple properties with summary', async () => {
    mockPrisma.property.findMany.mockResolvedValue(sampleProperties);
    mockAvm.estimateValue
      .mockResolvedValueOnce(makeValuation('5000000000', 0.85, 75000000))
      .mockResolvedValueOnce(makeValuation('8000000000', 0.90, 80000000));

    const query = new ValuationComparisonQuery(['prop-1', 'prop-2']);
    const result = await handler.execute(query);

    expect(result.properties).toHaveLength(2);
    expect(result.properties[0]!.propertyId).toBe('prop-1');
    expect(result.properties[0]!.address).toBe('10 Nguyễn Huệ');
    expect(result.properties[1]!.propertyId).toBe('prop-2');

    // Summary checks
    expect(result.summary.highestValue!.propertyId).toBe('prop-2');
    expect(result.summary.highestValue!.estimatedPrice).toBe('8000000000');
    expect(result.summary.lowestValue!.propertyId).toBe('prop-1');
    expect(result.summary.lowestValue!.estimatedPrice).toBe('5000000000');
    expect(result.summary.averagePricePerM2).toBe(77500000);
    expect(result.summary.averageConfidence).toBe(0.88);
  });

  it('handles properties where valuation fails gracefully', async () => {
    mockPrisma.property.findMany.mockResolvedValue(sampleProperties);
    mockAvm.estimateValue
      .mockResolvedValueOnce(makeValuation('5000000000', 0.85, 75000000))
      .mockRejectedValueOnce(new Error('AI service timeout'));

    const query = new ValuationComparisonQuery(['prop-1', 'prop-2']);
    const result = await handler.execute(query);

    expect(result.properties).toHaveLength(2);
    expect(result.properties[0]!.valuation).not.toBeNull();
    expect(result.properties[1]!.valuation).toBeNull();

    // Summary should only reflect the successful valuation
    expect(result.summary.highestValue!.propertyId).toBe('prop-1');
    expect(result.summary.lowestValue!.propertyId).toBe('prop-1');
  });

  it('returns null summary values when all valuations fail', async () => {
    mockPrisma.property.findMany.mockResolvedValue(sampleProperties);
    mockAvm.estimateValue
      .mockRejectedValueOnce(new Error('fail 1'))
      .mockRejectedValueOnce(new Error('fail 2'));

    const query = new ValuationComparisonQuery(['prop-1', 'prop-2']);
    const result = await handler.execute(query);

    expect(result.summary.highestValue).toBeNull();
    expect(result.summary.lowestValue).toBeNull();
    expect(result.summary.averagePricePerM2).toBe(0);
    expect(result.summary.averageConfidence).toBe(0);
  });

  it('handles unknown property IDs with empty details', async () => {
    mockPrisma.property.findMany.mockResolvedValue([]);
    mockAvm.estimateValue.mockRejectedValue(new Error('Not found'));

    const query = new ValuationComparisonQuery(['unknown-1', 'unknown-2']);
    const result = await handler.execute(query);

    expect(result.properties).toHaveLength(2);
    expect(result.properties[0]!.address).toBe('');
    expect(result.properties[0]!.district).toBe('');
    expect(result.properties[0]!.areaM2).toBe(0);
  });

  it('re-throws DomainException directly', async () => {
    const domainError = new DomainException('VALIDATION_ERROR', 'Too many properties');
    mockPrisma.property.findMany.mockRejectedValue(domainError);

    const query = new ValuationComparisonQuery(['prop-1', 'prop-2']);

    await expect(handler.execute(query)).rejects.toThrow(DomainException);
  });

  it('wraps unexpected errors in InternalServerErrorException', async () => {
    mockPrisma.property.findMany.mockRejectedValue(new Error('Connection refused'));

    const query = new ValuationComparisonQuery(['prop-1', 'prop-2']);

    await expect(handler.execute(query)).rejects.toThrow(InternalServerErrorException);
    expect(mockLogger.error).toHaveBeenCalled();
  });
});
