import { type IListingRepository } from '@modules/listings/domain/repositories/listing.repository';
import { type IPropertyRepository } from '@modules/listings/domain/repositories/property.repository';
import { type IDuplicateDetector } from '@modules/listings/domain/services/duplicate-detector';
import { type IPriceValidator } from '@modules/listings/domain/services/price-validator';
import { DomainException, ErrorCode } from '@modules/shared';
import { CreateListingCommand } from '../commands/create-listing/create-listing.command';
import { CreateListingHandler } from '../commands/create-listing/create-listing.handler';

describe('CreateListingHandler', () => {
  let handler: CreateListingHandler;
  let mockPropertyRepo: { [K in keyof IPropertyRepository]: ReturnType<typeof vi.fn> };
  let mockListingRepo: { [K in keyof IListingRepository]: ReturnType<typeof vi.fn> };
  let mockDuplicateDetector: { [K in keyof IDuplicateDetector]: ReturnType<typeof vi.fn> };
  let mockPriceValidator: { [K in keyof IPriceValidator]: ReturnType<typeof vi.fn> };
  let mockEventBus: { publish: ReturnType<typeof vi.fn> };
  let mockCache: { invalidateByPrefix: ReturnType<typeof vi.fn>; invalidate: ReturnType<typeof vi.fn>; getOrSet: ReturnType<typeof vi.fn> };

  beforeEach(() => {
    mockPropertyRepo = {
      findById: vi.fn(),
      save: vi.fn().mockResolvedValue(undefined),
      update: vi.fn(),
      addMedia: vi.fn(),
      findMediaByPropertyId: vi.fn(),
      deleteMedia: vi.fn(),
      countMediaByPropertyId: vi.fn(),
    };

    mockListingRepo = {
      findById: vi.fn(),
      findByIdWithProperty: vi.fn(),
      save: vi.fn().mockResolvedValue(undefined),
      update: vi.fn(),
      search: vi.fn(),
      findByStatus: vi.fn(),
      findBySellerId: vi.fn(),
    };

    mockDuplicateDetector = {
      findDuplicates: vi.fn().mockResolvedValue([]),
    };

    mockPriceValidator = {
      validate: vi.fn().mockResolvedValue({ isValid: true, isSuspicious: false, pricePerM2: 62_500_000, minPricePerM2: 15_000_000, maxPricePerM2: 200_000_000 }),
    };

    mockEventBus = { publish: vi.fn() };
    mockCache = {
      invalidateByPrefix: vi.fn().mockResolvedValue(undefined),
      invalidate: vi.fn(),
      getOrSet: vi.fn(),
    };

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

    handler = new CreateListingHandler(
      mockPropertyRepo as any,
      mockListingRepo as any,
      mockDuplicateDetector as any,
      mockPriceValidator as any,
      mockEventBus as any,
      mockCache as any,
      mockLogger as any,
    );
  });

  it('creates listing and property successfully', async () => {
    const command = new CreateListingCommand(
      'seller-1', 'SALE', 5_000_000_000n,
      'APARTMENT', 'Căn hộ đẹp', 'Mô tả chi tiết',
      '123 Nguyễn Huệ', 'Phường Bến Nghé', 'Quận 1', 'TP. Hồ Chí Minh',
      10.7769, 106.7009, 80,
    );

    const result = await handler.execute(command);

    expect(result.listingId).toBeDefined();
    expect(result.propertyId).toBeDefined();
    expect(result.status).toBe('DRAFT');
    expect(result.duplicateWarnings).toEqual([]);
    expect(mockPropertyRepo.save).toHaveBeenCalledTimes(1);
    expect(mockListingRepo.save).toHaveBeenCalledTimes(1);
    expect(mockEventBus.publish).toHaveBeenCalled();
    expect(mockCache.invalidateByPrefix).toHaveBeenCalled();
    expect(mockDuplicateDetector.findDuplicates).toHaveBeenCalledTimes(1);
  });

  it('creates listing with optional fields', async () => {
    const command = new CreateListingCommand(
      'seller-1', 'SALE', 3_000_000_000n,
      'HOUSE', 'Nhà phố', 'Mô tả',
      '456 Lê Lợi', 'Phường 1', 'Quận 3', 'TP. Hồ Chí Minh',
      10.78, 106.69, 120,
      100, 3, 2, 3, undefined, undefined, 'EAST', 2020, 'SỔ HỒNG',
    );

    const result = await handler.execute(command);

    expect(result.listingId).toBeDefined();
    expect(result.status).toBe('DRAFT');
  });

  it('throws ValidationException for invalid address', async () => {
    const command = new CreateListingCommand(
      'seller-1', 'SALE', 1_000_000_000n,
      'APARTMENT', 'Test', 'Test',
      '', '', '', '',
      10.77, 106.70, 50,
    );

    await expect(handler.execute(command)).rejects.toThrow();
  });

  it('throws ValidationException for invalid price', async () => {
    const command = new CreateListingCommand(
      'seller-1', 'SALE', -100n,
      'APARTMENT', 'Test', 'Test',
      '123 ABC', 'Ward', 'District', 'City',
      10.77, 106.70, 50,
    );

    await expect(handler.execute(command)).rejects.toThrow();
  });

  it('throws ValidationException for invalid geo coordinates', async () => {
    const command = new CreateListingCommand(
      'seller-1', 'SALE', 1_000_000_000n,
      'APARTMENT', 'Test', 'Test',
      '123 ABC', 'Ward', 'District', 'City',
      999, 999, 50,
    );

    await expect(handler.execute(command)).rejects.toThrow();
  });

  it('rejects hard duplicate (same agent + addressMatch + within 50m) with 409 and does NOT persist', async () => {
    mockDuplicateDetector.findDuplicates.mockResolvedValue([
      {
        listingId: 'listing-existing',
        propertyId: 'property-existing',
        title: 'Căn hộ đã đăng',
        address: '123 Nguyễn Huệ',
        district: 'Quận 1',
        distanceMeters: 30,
        addressMatch: true,
        sameAgent: true,
        propertyType: 'APARTMENT',
      },
    ]);

    const command = new CreateListingCommand(
      'seller-1', 'SALE', 5_000_000_000n,
      'APARTMENT', 'Căn hộ', 'Mô tả',
      '123 Nguyễn Huệ', 'Phường Bến Nghé', 'Quận 1', 'TP. Hồ Chí Minh',
      10.7769, 106.7009, 80,
      undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined,
      undefined, undefined, undefined, undefined,
      'agent-1', // agentId — triggers hard duplicate path
    );

    await expect(handler.execute(command)).rejects.toMatchObject({
      errorCode: ErrorCode.CONFLICT,
    });
    await expect(handler.execute(command)).rejects.toBeInstanceOf(DomainException);
    // Property and Listing must NOT have been persisted when 409 fires.
    expect(mockPropertyRepo.save).not.toHaveBeenCalled();
    expect(mockListingRepo.save).not.toHaveBeenCalled();
  });

  it('returns soft warnings when duplicates are nearby but not a hard match', async () => {
    mockDuplicateDetector.findDuplicates.mockResolvedValue([
      {
        listingId: 'listing-near',
        propertyId: 'property-near',
        title: 'Căn hộ lân cận',
        address: '125 Nguyễn Huệ',
        district: 'Quận 1',
        distanceMeters: 45,
        addressMatch: false,
        sameAgent: false,
        propertyType: 'APARTMENT',
      },
    ]);

    const command = new CreateListingCommand(
      'seller-1', 'SALE', 5_000_000_000n,
      'APARTMENT', 'Căn hộ mới', 'Mô tả',
      '123 Nguyễn Huệ', 'Phường Bến Nghé', 'Quận 1', 'TP. Hồ Chí Minh',
      10.7769, 106.7009, 80,
      undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined,
      undefined, undefined, undefined, undefined,
      'agent-1',
    );

    const result = await handler.execute(command);

    expect(result.duplicateWarnings).toHaveLength(1);
    expect(result.duplicateWarnings[0]!.addressMatch).toBe(false);
    expect(result.duplicateWarnings[0]!.sameAgent).toBe(false);
    expect(mockPropertyRepo.save).toHaveBeenCalledTimes(1);
    expect(mockListingRepo.save).toHaveBeenCalledTimes(1);
  });
});
