import { describe, it, expect, vi, beforeEach } from 'vitest';
import { NotFoundException } from '@modules/shared';
import { ListingsController } from '../controllers/listings.controller';

// ---------------------------------------------------------------------------
// QRCode mock — avoids canvas / native binary deps in test environment
// ---------------------------------------------------------------------------
vi.mock('qrcode', () => ({
  toBuffer: vi.fn().mockResolvedValue(Buffer.from('PNG_BYTES')),
  toString: vi.fn().mockResolvedValue('<svg></svg>'),
}));

describe('ListingsController', () => {
  let controller: ListingsController;
  let mockCommandBus: { execute: ReturnType<typeof vi.fn> };
  let mockQueryBus: { execute: ReturnType<typeof vi.fn> };

  beforeEach(() => {
    mockCommandBus = { execute: vi.fn() };
    mockQueryBus = { execute: vi.fn() };
    controller = new ListingsController(mockCommandBus as any, mockQueryBus as any);
  });

  describe('createListing', () => {
    it('should execute CreateListingCommand via command bus', async () => {
      const mockResult = {
        listingId: 'listing-1',
        propertyId: 'prop-1',
        status: 'DRAFT',
        duplicateWarnings: [],
      };
      mockCommandBus.execute.mockResolvedValue(mockResult);

      const dto = {
        transactionType: 'SALE' as const,
        priceVND: 5_000_000_000n,
        propertyType: 'APARTMENT' as const,
        title: 'Căn hộ đẹp Quận 1',
        description: 'Mô tả chi tiết căn hộ',
        address: '123 Nguyễn Huệ',
        ward: 'Bến Nghé',
        district: 'Quận 1',
        city: 'Hồ Chí Minh',
        latitude: 10.7769,
        longitude: 106.7009,
        areaM2: 80,
      };
      const user = { sub: 'seller-1', email: 'test@example.com', role: 'SELLER' };

      const result = await controller.createListing(dto as any, user as any);

      expect(result).toEqual(mockResult);
      expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
    });
  });

  describe('getListing', () => {
    it('should execute GetListingQuery via query bus', async () => {
      const mockDetail = {
        id: 'listing-1',
        status: 'ACTIVE',
        transactionType: 'SALE',
        priceVND: '5000000000',
      };
      mockQueryBus.execute.mockResolvedValue(mockDetail);

      const result = await controller.getListing('listing-1');

      expect(result).toEqual(mockDetail);
      expect(mockQueryBus.execute).toHaveBeenCalledTimes(1);
    });

    it('should throw NotFoundException when listing does not exist', async () => {
      mockQueryBus.execute.mockResolvedValue(null);

      await expect(controller.getListing('nonexistent-id')).rejects.toThrow(NotFoundException);
    });

    it('should include the listing ID in the NotFoundException message', async () => {
      mockQueryBus.execute.mockResolvedValue(null);

      await expect(controller.getListing('abc123')).rejects.toThrow(
        expect.objectContaining({ message: expect.stringContaining('abc123') }),
      );
    });
  });

  describe('searchListings', () => {
    it('should execute SearchListingsQuery via query bus', async () => {
      const mockResults = {
        data: [],
        total: 0,
        page: 1,
        limit: 20,
        totalPages: 0,
      };
      mockQueryBus.execute.mockResolvedValue(mockResults);

      const dto = { status: 'ACTIVE' as const, page: 1, limit: 20 };
      const result = await controller.searchListings(dto as any);

      expect(result).toEqual(mockResults);
      expect(mockQueryBus.execute).toHaveBeenCalledTimes(1);
    });
  });

  describe('getPriceHistory', () => {
    it('should execute GetPriceHistoryQuery via query bus', async () => {
      const mockHistory = [
        { id: 'ph-1', oldPrice: '5000000000', newPrice: '6000000000', source: 'manual_update', changedAt: '2026-04-16T00:00:00.000Z' },
      ];
      mockQueryBus.execute.mockResolvedValue(mockHistory);

      const result = await controller.getPriceHistory('listing-1');

      expect(result).toEqual(mockHistory);
      expect(mockQueryBus.execute).toHaveBeenCalledTimes(1);
    });

    it('should return empty array when no price history exists', async () => {
      mockQueryBus.execute.mockResolvedValue([]);

      const result = await controller.getPriceHistory('listing-no-history');

      expect(result).toEqual([]);
      expect(mockQueryBus.execute).toHaveBeenCalledTimes(1);
    });
  });

  describe('updateListing', () => {
    it('should execute UpdateListingCommand via command bus', async () => {
      const mockResult = {
        listingId: 'listing-1',
        status: 'DRAFT',
        updatedFields: ['title', 'priceVND'],
        resubmittedForModeration: false,
      };
      mockCommandBus.execute.mockResolvedValue(mockResult);

      const dto = {
        title: 'Căn hộ 3PN view sông mới',
        priceVND: 6_000_000_000n,
      };
      const user = { sub: 'seller-1', email: 'seller@example.com', role: 'SELLER' };

      const result = await controller.updateListing('listing-1', dto as any, user as any);

      expect(result).toEqual(mockResult);
      expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
    });

    it('should pass all optional fields to the command', async () => {
      const mockResult = {
        listingId: 'listing-1',
        status: 'PENDING_REVIEW',
        updatedFields: ['title', 'description', 'priceVND', 'rentPriceMonthly', 'amenities', 'mediaOrder'],
        resubmittedForModeration: true,
      };
      mockCommandBus.execute.mockResolvedValue(mockResult);

      const dto = {
        title: 'Tiêu đề cập nhật',
        description: 'Mô tả chi tiết hơn cho căn hộ',
        priceVND: 5_500_000_000n,
        rentPriceMonthly: 25_000_000n,
        amenities: ['Hồ bơi', 'Gym'],
        mediaOrder: [{ mediaId: 'media-1', order: 0 }],
      };
      const user = { sub: 'seller-1', email: 'seller@example.com', role: 'SELLER' };

      const result = await controller.updateListing('listing-1', dto as any, user as any);

      expect(result.resubmittedForModeration).toBe(true);
      expect(result.status).toBe('PENDING_REVIEW');
      expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
    });
  });

  describe('updateStatus', () => {
    it('should execute UpdateListingStatusCommand via command bus', async () => {
      mockCommandBus.execute.mockResolvedValue({ status: 'ACTIVE' });

      const dto = { status: 'ACTIVE' as const };
      const user = { sub: 'admin-1', email: 'admin@example.com', role: 'ADMIN' };

      const result = await controller.updateStatus('listing-1', dto as any, user as any);

      expect(result).toEqual({ status: 'ACTIVE' });
      expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
    });
  });

  describe('getPendingModeration', () => {
    it('should execute GetPendingModerationQuery with defaults', async () => {
      const mockResults = { data: [], total: 0, page: 1, limit: 20, totalPages: 0 };
      mockQueryBus.execute.mockResolvedValue(mockResults);

      const result = await controller.getPendingModeration();

      expect(result).toEqual(mockResults);
      expect(mockQueryBus.execute).toHaveBeenCalledTimes(1);
    });

    it('should execute GetPendingModerationQuery with custom page/limit', async () => {
      const mockResults = { data: [], total: 0, page: 2, limit: 10, totalPages: 0 };
      mockQueryBus.execute.mockResolvedValue(mockResults);

      const result = await controller.getPendingModeration(2, 10);

      expect(result).toEqual(mockResults);
    });
  });

  describe('moderateListing', () => {
    it('should execute ModerateListingCommand via command bus', async () => {
      mockCommandBus.execute.mockResolvedValue({ status: 'ACTIVE' });

      const dto = { action: 'approve' as const, moderationScore: 90, notes: 'Hợp lệ' };
      const user = { sub: 'admin-1', email: 'admin@example.com', role: 'ADMIN' };

      const result = await controller.moderateListing('listing-1', dto as any, user as any);

      expect(result).toEqual({ status: 'ACTIVE' });
      expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
    });
  });

  describe('getQrCode', () => {
    function makeRes() {
      const headers: Record<string, string> = {};
      let body: unknown;
      return {
        set: vi.fn((h: Record<string, string>) => Object.assign(headers, h)),
        send: vi.fn((b: unknown) => { body = b; }),
        _headers: headers,
        _body: () => body,
      };
    }

    it('returns a PNG buffer and correct headers for format=png (default)', async () => {
      mockQueryBus.execute.mockResolvedValue({ id: 'listing-1', status: 'ACTIVE' });
      const res = makeRes();

      await controller.getQrCode('listing-1', res as any, 300, undefined);

      expect(res.set).toHaveBeenCalledWith(
        expect.objectContaining({ 'Content-Type': 'image/png' }),
      );
      expect(res.send).toHaveBeenCalledWith(expect.any(Buffer));
    });

    it('returns SVG string and correct headers for format=svg', async () => {
      mockQueryBus.execute.mockResolvedValue({ id: 'listing-1', status: 'ACTIVE' });
      const res = makeRes();

      await controller.getQrCode('listing-1', res as any, 300, 'svg');

      expect(res.set).toHaveBeenCalledWith(
        expect.objectContaining({ 'Content-Type': 'image/svg+xml' }),
      );
      expect(res.send).toHaveBeenCalledWith('<svg></svg>');
    });

    it('sets Cache-Control: public, max-age=86400 on QR response', async () => {
      mockQueryBus.execute.mockResolvedValue({ id: 'listing-1', status: 'ACTIVE' });
      const res = makeRes();

      await controller.getQrCode('listing-1', res as any, 300, undefined);

      expect(res.set).toHaveBeenCalledWith(
        expect.objectContaining({ 'Cache-Control': 'public, max-age=86400' }),
      );
    });

    it('throws NotFoundException when listing does not exist', async () => {
      mockQueryBus.execute.mockResolvedValue(null);
      const res = makeRes();

      await expect(
        controller.getQrCode('nonexistent', res as any, 300, undefined),
      ).rejects.toThrow(NotFoundException);
    });
  });
});
