import { InternalServerErrorException } from '@nestjs/common';
import { type IListingRepository } from '@modules/listings/domain/repositories/listing.repository';
import { GetListingHandler } from '../queries/get-listing/get-listing.handler';
import { GetListingQuery } from '../queries/get-listing/get-listing.query';

const baseListingDetail = {
  id: 'listing-1',
  status: 'ACTIVE',
  transactionType: 'SALE',
  priceVND: '5000000000',
  pricePerM2: 62500000,
  rentPriceMonthly: null,
  commissionPct: 2.0,
  viewCount: 10,
  saveCount: 2,
  inquiryCount: 5,
  isFeatured: false,
  featuredUntil: null,
  publishedAt: '2026-01-01T00:00:00.000Z',
  createdAt: '2026-01-01T00:00:00.000Z',
  valuationEstimate: null,
  agentQualityScore: { score: 78, tier: 'GOLD' },
  similarCount: 3,
  property: { id: 'prop-1', title: 'Căn hộ Q1', district: 'Quận 1', city: 'Hồ Chí Minh', areaM2: 80 },
  seller: { id: 'seller-1', fullName: 'Nguyễn Văn A', phone: '0901234567' },
  agent: { id: 'agent-1', userId: 'user-a', agency: 'Đất Xanh' },
};

describe('GetListingHandler', () => {
  let handler: GetListingHandler;
  let mockListingRepo: { [K in keyof IListingRepository]: ReturnType<typeof vi.fn> };
  let mockAvmService: { estimateValue: ReturnType<typeof vi.fn> };
  let mockCache: { getOrSet: ReturnType<typeof vi.fn>; invalidate: ReturnType<typeof vi.fn>; invalidateByPrefix: ReturnType<typeof vi.fn> };
  let mockLogger: { log: ReturnType<typeof vi.fn>; error: ReturnType<typeof vi.fn>; warn: ReturnType<typeof vi.fn>; debug: ReturnType<typeof vi.fn> };

  beforeEach(() => {
    mockListingRepo = {
      findById: vi.fn(),
      findByIdWithProperty: vi.fn(),
      save: vi.fn(),
      update: vi.fn(),
      search: vi.fn(),
      findByStatus: vi.fn(),
      findBySellerId: vi.fn(),
      findSimilar: vi.fn(),
    };

    mockAvmService = {
      estimateValue: vi.fn().mockResolvedValue({
        estimatedPrice: '5200000000',
        confidence: 0.87,
        modelVersion: 'v2',
        comparables: [],
      }),
    };

    mockCache = {
      getOrSet: vi.fn(),
      invalidate: vi.fn(),
      invalidateByPrefix: vi.fn(),
    };

    mockLogger = {
      log: vi.fn(),
      error: vi.fn(),
      warn: vi.fn(),
      debug: vi.fn(),
    };

    handler = new GetListingHandler(
      mockListingRepo as any,
      mockAvmService as any,
      mockCache as any,
      mockLogger as any,
    );
  });

  /**
   * Helper: configure cache mock to call through to the provided loader,
   * allowing tests to control what the repo / AVM returns.
   */
  function callThrough() {
    mockCache.getOrSet.mockImplementation(async (_key: string, fn: () => Promise<unknown>) => fn());
  }

  it('returns listing detail via cache', async () => {
    callThrough();
    mockListingRepo.findByIdWithProperty.mockResolvedValue(baseListingDetail);

    const result = await handler.execute(new GetListingQuery('listing-1'));

    expect(result).not.toBeNull();
    expect(result!.id).toBe('listing-1');
    expect(mockCache.getOrSet).toHaveBeenCalled();
  });

  it('returns null when listing not found', async () => {
    callThrough();
    mockListingRepo.findByIdWithProperty.mockResolvedValue(null);

    const result = await handler.execute(new GetListingQuery('nonexistent'));

    expect(result).toBeNull();
  });

  it('does not cache not-found results', async () => {
    callThrough();
    mockListingRepo.findByIdWithProperty.mockResolvedValue(null);

    const result = await handler.execute(new GetListingQuery('nonexistent'));

    expect(result).toBeNull();
  });

  it('uses cache key with listing id', async () => {
    callThrough();
    mockListingRepo.findByIdWithProperty.mockResolvedValue(baseListingDetail);

    await handler.execute(new GetListingQuery('listing-1'));

    expect(mockCache.getOrSet).toHaveBeenCalledWith(
      expect.stringContaining('listing-1'),
      expect.any(Function),
      expect.anything(),
      expect.anything(),
    );
  });

  it('throws InternalServerErrorException on unexpected errors', async () => {
    mockCache.getOrSet.mockRejectedValue(new Error('Redis connection failed'));

    await expect(handler.execute(new GetListingQuery('listing-1'))).rejects.toThrow(InternalServerErrorException);
    expect(mockLogger.error).toHaveBeenCalled();
  });

  // ── Enrichment: valuationEstimate ──────────────────────────────────────────

  it('attaches valuationEstimate from AVM when available', async () => {
    callThrough();
    mockListingRepo.findByIdWithProperty.mockResolvedValue(baseListingDetail);

    const result = await handler.execute(new GetListingQuery('listing-1'));

    expect(result!.valuationEstimate).not.toBeNull();
    expect(result!.valuationEstimate!.value).toBe('5200000000');
    expect(result!.valuationEstimate!.confidence).toBe(0.87);
    expect(result!.valuationEstimate!.modelVersion).toBe('v2');
    expect(result!.valuationEstimate!.estimatedAt).toBeDefined();
  });

  it('returns null valuationEstimate when AVM service throws', async () => {
    callThrough();
    mockListingRepo.findByIdWithProperty.mockResolvedValue(baseListingDetail);
    mockAvmService.estimateValue.mockRejectedValue(new Error('AVM unavailable'));

    const result = await handler.execute(new GetListingQuery('listing-1'));

    expect(result).not.toBeNull();
    expect(result!.valuationEstimate).toBeNull();
    expect(mockLogger.warn).toHaveBeenCalled();
  });

  // ── Enrichment: inquiryCount access gating ─────────────────────────────────

  it('exposes inquiryCount to the listing owner', async () => {
    callThrough();
    mockListingRepo.findByIdWithProperty.mockResolvedValue(baseListingDetail);

    const caller = { userId: 'seller-1', role: 'USER' };
    const result = await handler.execute(new GetListingQuery('listing-1', caller));

    expect(result!.inquiryCount).toBe(5);
  });

  it('exposes inquiryCount to an ADMIN', async () => {
    callThrough();
    mockListingRepo.findByIdWithProperty.mockResolvedValue(baseListingDetail);

    const caller = { userId: 'admin-user', role: 'ADMIN' };
    const result = await handler.execute(new GetListingQuery('listing-1', caller));

    expect(result!.inquiryCount).toBe(5);
  });

  it('hides inquiryCount from anonymous / public callers', async () => {
    callThrough();
    mockListingRepo.findByIdWithProperty.mockResolvedValue(baseListingDetail);

    const result = await handler.execute(new GetListingQuery('listing-1'));

    expect(result!.inquiryCount).toBeNull();
  });

  it('hides inquiryCount from a non-owner authenticated user', async () => {
    callThrough();
    mockListingRepo.findByIdWithProperty.mockResolvedValue(baseListingDetail);

    const caller = { userId: 'other-user', role: 'USER' };
    const result = await handler.execute(new GetListingQuery('listing-1', caller));

    expect(result!.inquiryCount).toBeNull();
  });

  // ── Enrichment: agentQualityScore ─────────────────────────────────────────

  it('includes agentQualityScore from the base repository result', async () => {
    callThrough();
    mockListingRepo.findByIdWithProperty.mockResolvedValue(baseListingDetail);

    const result = await handler.execute(new GetListingQuery('listing-1'));

    expect(result!.agentQualityScore).toEqual({ score: 78, tier: 'GOLD' });
  });

  it('returns null agentQualityScore when no agent is assigned', async () => {
    callThrough();
    mockListingRepo.findByIdWithProperty.mockResolvedValue({
      ...baseListingDetail,
      agentQualityScore: null,
      agent: null,
    });

    const result = await handler.execute(new GetListingQuery('listing-1'));

    expect(result!.agentQualityScore).toBeNull();
  });

  // ── Enrichment: similarCount ───────────────────────────────────────────────

  it('includes similarCount from the base repository result', async () => {
    callThrough();
    mockListingRepo.findByIdWithProperty.mockResolvedValue(baseListingDetail);

    const result = await handler.execute(new GetListingQuery('listing-1'));

    expect(result!.similarCount).toBe(3);
  });
});
