import { InternalServerErrorException } from '@nestjs/common';
import { type CacheService } from '@modules/shared/infrastructure/cache.service';
import { DomainException, NotFoundException } from '@modules/shared';
import { ValuationEntity } from '../../domain/entities/valuation.entity';
import { type IValuationRepository } from '../../domain/repositories/valuation.repository';
import { ValuationExplanationHandler } from '../queries/valuation-explanation/valuation-explanation.handler';
import { ValuationExplanationQuery } from '../queries/valuation-explanation/valuation-explanation.query';

describe('ValuationExplanationHandler', () => {
  let handler: ValuationExplanationHandler;
  let mockRepo: { [K in keyof IValuationRepository]: ReturnType<typeof vi.fn> };
  let mockLogger: { error: ReturnType<typeof vi.fn> };

  beforeEach(() => {
    mockRepo = {
      findById: vi.fn(),
      findByPropertyId: vi.fn(),
      findLatestByPropertyId: vi.fn(),
      save: vi.fn(),
    };
    mockLogger = { error: vi.fn() };
    const mockCache = {
      getOrSet: vi.fn((_key: string, loader: () => Promise<unknown>) => loader()),
    } as unknown as CacheService;
    handler = new ValuationExplanationHandler(
      mockRepo as any,
      mockCache,
      mockLogger as any,
    );
  });

  it('returns explanation with top drivers from drivers array', async () => {
    const entity = new ValuationEntity(
      'val-1',
      {
        propertyId: 'prop-1',
        estimatedPrice: 5_000_000_000n,
        confidence: 0.87,
        pricePerM2: 75_000_000,
        comparables: [
          { propertyId: 'c1', address: 'A', district: 'D1', priceVND: '5000000000', pricePerM2: 75000000, areaM2: 60, propertyType: 'APARTMENT', distanceMeters: 100, soldAt: '2026-01-01' },
        ],
        features: {
          drivers: [
            { feature: 'location', importance: 0.45 },
            { feature: 'area', importance: -0.22 },
            { feature: 'year_built', importance: 0.12 },
          ],
        },
        modelVersion: 'avm-v2.0',
      },
      new Date('2026-04-15T10:00:00Z'),
    );
    mockRepo.findById.mockResolvedValue(entity);

    const result = await handler.execute(new ValuationExplanationQuery('val-1'));

    expect(result.valuationId).toBe('val-1');
    expect(result.propertyId).toBe('prop-1');
    expect(result.modelVersion).toBe('avm-v2.0');
    expect(result.estimatedPrice).toBe('5000000000');
    expect(result.topDrivers).toHaveLength(3);
    // Sorted by |importance| descending
    expect(result.topDrivers[0]!.feature).toBe('location');
    expect(result.topDrivers[1]!.feature).toBe('area');
    expect(result.comparables).toHaveLength(1);
    expect(result.confidenceExplanation).toContain('cao');
    expect(result.valuedAt).toBe('2026-04-15T10:00:00.000Z');
  });

  it('falls back to object-of-numbers feature importances', async () => {
    const entity = new ValuationEntity(
      'val-2',
      {
        propertyId: 'prop-2',
        estimatedPrice: 3_000_000_000n,
        confidence: 0.55,
        pricePerM2: 50_000_000,
        comparables: [],
        features: { location: 0.6, area: 0.2, foo: 'not-number' },
        modelVersion: 'avm-v1.0',
      },
      new Date('2026-03-01T00:00:00Z'),
    );
    mockRepo.findById.mockResolvedValue(entity);

    const result = await handler.execute(new ValuationExplanationQuery('val-2'));

    expect(result.topDrivers.map((d) => d.feature)).toEqual(['location', 'area']);
    expect(result.comparables).toEqual([]);
  });

  it('throws NotFoundException when valuation does not exist', async () => {
    mockRepo.findById.mockResolvedValue(null);

    await expect(
      handler.execute(new ValuationExplanationQuery('missing')),
    ).rejects.toThrow(NotFoundException);
  });

  it('re-throws DomainException directly', async () => {
    const domainError = new DomainException('NOT_FOUND' as any, 'Valuation not found');
    mockRepo.findById.mockRejectedValue(domainError);

    await expect(
      handler.execute(new ValuationExplanationQuery('v-err')),
    ).rejects.toThrow(DomainException);
  });

  it('wraps unexpected errors in InternalServerErrorException', async () => {
    mockRepo.findById.mockRejectedValue(new Error('DB down'));

    await expect(
      handler.execute(new ValuationExplanationQuery('v-err')),
    ).rejects.toThrow(InternalServerErrorException);
    expect(mockLogger.error).toHaveBeenCalled();
  });
});
